1. Aprendizado de programação para Android
O PDF do documento está disponível |AQUI|.
Os exemplos do documento estão disponíveis |AQUI|.
1.1. Introduction
1.1.1. Índice
Este documento é uma reformulação de vários documentos existentes:
- Introdução à programação de tablets Android por meio de exemplos;
- Controlando um Arduino com um tablet Android;
- Introdução à programação de tablets Android com exemplos – versão 2
e apresenta as seguintes novidades:
- o documento 1 apresentava uma arquitetura chamada AVAT (Atividade-Visualizações-Ações-Tarefas) para facilitar a programação assíncrona em um aplicativo Android. Neste documento, a biblioteca padrão RxJava é utilizada para gerenciar as ações assíncronas;
- o documento 2 utilizava o Eclipse IDE com um plugin para Android. Este documento utiliza o Android Studio;
- o documento 3 foi reproduzido tal como está;
- o documento 4 utilizava a biblioteca [Android Annotations] (AA) com o IntelliJ IDE Community Edition. Este documento reproduz na íntegra o documento 4 com as seguintes diferenças:
- o IDE agora é o Android Studio;
- o sistema de compilação é o Gradle para todos os projetos de cliente ou servidor (no documento 4, às vezes se utilizava o Maven)
- a programação assíncrona é realizada com a biblioteca RxJava (no documento 4, utilizava-se a biblioteca AA);
- este documento explora áreas que não foram abordadas, ou foram pouco abordadas, nos documentos anteriores:
- o conceito de adjacência de fragmentos;
- o salvamento/restauração da atividade e de seus fragmentos;
- o ciclo de vida dos fragmentos;
Por fim, ele apresenta a estrutura básica de um cliente Android que se comunica com um serviço web / jSON, na qual se agrupam um grande número de elementos que aparecem regularmente nesse tipo de cliente. Essa estrutura básica é utilizada em todos os exemplos a partir do capítulo 2. Essa é a parte realmente inovadora do documento.
São apresentados os seguintes exemplos:
Natureza | |
Importação de um projeto Android existente | |
Um projeto básico do Android | |
Um projeto [Android Annotations] básico | |
Visões e eventos | |
Navegação entre visões | |
Navegação por abas | |
Uso da biblioteca [Android Annotations] com o Gradle | |
Gerenciamento de fragmentos em um aplicativo Android | |
Navegação entre telas revisitada | |
Arquitetura de duas camadas | |
Arquitetura cliente/servidor | |
Gerenciar a assincronia com RxJava | |
Componentes de entrada de dados | |
Uso de um modelo de visualizações | |
O componente ListView | |
Como usar um menu | |
Uso de uma classe pai para os fragmentos | |
Salvar e restaurar o estado da atividade e dos fragmentos | |
Cliente de previsão do tempo | |
Estrutura básica de um cliente Android que se comunica com um serviço web / jSON. Nela, são agrupados diversos elementos que aparecem com frequência nesse tipo de cliente Android. | |
Gerenciamento de consultas em um consultório médico | |
Exercício prático – Gerenciamento de uma folha de pagamento básica | |
Exercício prático – Controle de placas Arduino |
Este documento foi utilizado no último ano da Escola de Engenharia IstiA da Universidade de Angers [istia.univ-angers.fr]. Isso explica o tom, por vezes um pouco peculiar, do texto. Os dois exercícios práticos são textos de TP, dos quais apresentamos apenas as linhas gerais da solução. Cabe ao leitor elaborá-la.
O código-fonte dos exemplos está disponível |ICI|. Para executar esses exemplos, você deve seguir o procedimento descrito no parágrafo 6.12.
Este documento é um material de introdução à programação para Android. Não pretende ser exaustivo. Destina-se essencialmente a iniciantes.
O site de referência para programação Android está em URL [http://developer.android.com/guide/components/index.html]. É lá que você deve acessar para ter uma visão geral da programação Android.
1.1.2. Pré-requisitos
O pré-requisito para o uso ideal deste documento é um bom domínio da linguagem Java.
1.1.3. As ferramentas utilizadas
Os exemplos a seguir foram testados no seguinte ambiente:
- computador com Windows 10 Pro de 64 bits;
- JDK 1.8;
- Android SDK API 23;
- Android Studio, versão 2.1;
- Emulador Genymotion, versão 2.6.0;
Para seguir este documento, você deve instalar:
- um JDK (ver parágrafo 6.8);
- o gerenciador de emuladores Android Genymotion (ver parágrafo 6.9);
- o gerenciador de dependências Maven (ver parágrafo 6.10);
- o IDE [Android Studio] (ver parágrafo 6.11);
1.2. Exemplo-01: importação de um exemplo do Android
1.2.1. Criação do projeto
Vamos criar, com o Android Studio, um primeiro projeto Android. Primeiro, vamos criar uma pasta [exemples] vazia, onde serão colocados todos os nossos projetos:
![]() |
em seguida, vamos criar um projeto com o Android Studio. Primeiramente, vamos importar um dos exemplos fornecidos com o IDE [1-5]:
![]() |

![]() | ![]() |
A importação do projeto pode resultar em erros devido à incompatibilidade entre o ambiente utilizado na criação do projeto e aquele utilizado aqui para sua execução. Esta é uma oportunidade para ver como resolver esse tipo de erro. Aqui, temos o seguinte erro:
![]() | ![]() |
O projeto importado está configurado pelo seguinte arquivo [build.gradle] [2]:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
apply plugin: 'com.android.application'
repositories {
jcenter()
}
dependencies {
compile "com.android.support:support-v4:23.3.0"
compile "com.android.support:support-v13:23.3.0"
compile "com.android.support:cardview-v7:23.3.0"
}
// A compilação de exemplo utiliza vários diretórios para
// separar o código padrão e comum do
// do código principal do exemplo.
List<String> dirs = [
'main', // código principal da amostra; veja aqui o que há de interessante.
'common', // componentes reutilizados por vários exemplos
'template'] // código padrão gerado pelo processo de modelo de exemplo
android {
compileSdkVersion 21
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 21
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
sourceSets {
main {
dirs.each { dir ->
java.srcDirs "src/${dir}/java"
res.srcDirs "src/${dir}/res"
}
}
androidTest.setRoot('tests')
androidTest.java.srcDirs = ['tests/src']
}
aaptOptions {
noCompress "pdf"
}
}
- O erro relatado se deve às linhas 31, 34 e 35: não temos o SDK 21. Substituímos essa versão pela versão 23, que temos disponível.
No arquivo [build.gradle], o Android Studio faz sugestões como as abaixo:
![]() |
Para aceitar as sugestões, selecionamos [alt-entrée] na sugestão:
![]() |
Também pode ocorrer um erro relacionado à versão do Gradle:
![]() |
Esse erro decorre de uma incompatibilidade entre a versão do Gradle solicitada pelo arquivo [build.gradle] do projeto (a versão 2.10, linha 6 abaixo):
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
e a versão indicada no arquivo [<projet>/gradle/wrapper/gradle-wrapper.properties]:
#Qua, 10 de abril, 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
Na linha 6, acima, é preciso substituir 2,8 por 2,10.
Para acessar o arquivo [<projet>/gradle/wrapper/gradle-wrapper.properties], é preciso usar a perspectiva do projeto:
![]() | ![]() | ![]() |
Depois de corrigir isso, é possível compilar o aplicativo [1], iniciar o emulador Genymotion [2] e, em seguida, executar o projeto [3]:
![]() | ![]() | ![]() |
![]() |

Vamos encerrar o aplicativo:
![]() |
Agora podemos fechar o projeto. Vamos criar um novo.
![]() |
1.2.2. Algumas observações sobre o IDE
1.2.2.1. As perspectivas
O Android Studio (AS) oferece diferentes perspectivas para trabalhar com um projeto. Usaremos principalmente duas delas:
- a perspectiva [Android] [1]:
- a perspectiva [Project] [4];
![]() | ![]() |
![]() |
Na maioria das vezes, trabalharemos com a perspectiva [Android]. Ao duplicarmos um projeto em outro, precisaremos da perspectiva [Project].
1.2.2.2. Gerenciamento da execução
Existem várias maneiras de executar/parar/reexecutar um projeto AS. Em primeiro lugar, há os botões da barra de ferramentas:
![]() | ![]() | ![]() |
O botão [Rerun] [3] interrompe a execução do projeto [2] e, em seguida, o reinicia [1].
1.2.2.3. Gerenciamento do cache
O Android Studio mantém um cache dos projetos que gerencia com o objetivo de tornar o IDE o mais responsivo possível. Na versão Android 2.1 (maio de 2016), muitas vezes esse cache não levava em conta as alterações de código que acabávamos de fazer. Nesse caso, é necessário invalidar esse cache:
![]() | ![]() |
Com o Android 2.1 (maio de 2016), a operação anterior precisava ser repetida várias vezes e, às vezes, isso não era suficiente para resolver a anomalia detectada. A solução foi desativar a tecnologia [Instant Run]:
![]() | ![]() |
- em [3-4], tudo foi desativado;
Em todas as etapas a seguir, trabalhamos com essa configuração do cache e não encontramos problemas.
1.2.2.4. Gerenciamento de logs
Durante a execução de um projeto, os logs são exibidos no monitor do Android:
![]() | ![]() |
Na aba [Android Monitor] [1], os logs são exibidos na aba [logcat] [2]. O botão [3] permite apagar os registros. Esse botão é útil quando se deseja visualizar os registros de uma ação específica:
- exclui-se os logs;
- no dispositivo Android, realiza-se a ação cujos registros se deseja obter;
- os registros que aparecem a partir daí são aqueles relacionados à ação realizada;
Existem vários níveis de logs [4]. Por padrão, o modo [Verbose] está selecionado. Isso significa que os logs de todos os níveis são exibidos. Com o [4], é possível selecionar um nível específico.
Os logs são muito úteis para saber em quais momentos da execução de um projeto determinados métodos são exibidos. Recorreremos a eles com frequência. Consideremos o código da classe [MainActivity] do projeto [Exemple-01]:
![]() |
package com.example.android.pdfrendererbasic;
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
public class MainActivity extends Activity {
public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_real);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
.add(R.id.container, new PdfRendererBasicFragment(),
FRAGMENT_PDF_RENDERER_BASIC)
.commit();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_info:
new AlertDialog.Builder(this)
.setMessage(R.string.intro_message)
.setPositiveButton(android.R.string.ok, null)
.show();
return true;
}
return super.onOptionsItemSelected(item);
}
}
Acima, os métodos [onCreate, ligne 14] e [onCreateOptionsMenu, ligne 26] são métodos da classe pai [Activity] (linha 9). Eles são chamados em diferentes momentos do ciclo de vida do aplicativo. Às vezes, são executados várias vezes. Mesmo ao ler a documentação, às vezes é difícil determinar se um método do ciclo de vida será executado antes ou depois de um método que nós mesmos tenhamos escrito. No entanto, muitas vezes é importante saber essa informação. Nesse caso, podemos inserir registros de log como os mostrados abaixo:
public class MainActivity extends Activity {
public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
super.onCreate(savedInstanceState);
...
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("MainActivity","onCreateOptionsMenu");
getMenuInflater().inflate(R.menu.main, menu);
...
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("MainActivity","onOptionsItemSelected");
switch (item.getItemId()) {
...
}
}
- nas linhas 7, 14 e 21, utiliza-se a classe [Log]. Essa classe permite gravar logs no console do Android [logcat]. Os logs são classificados em vários níveis (info, warning, debug, verbose, error). [Log.d] exibe registros de nível [debug]. Seu primeiro argumento é a fonte da mensagem de registro. De fato, diversas fontes podem emitir mensagens no console de registros. Para diferenciá-las, utiliza-se esse primeiro argumento. O segundo argumento é a mensagem a ser gravada no console de registros;
Se executarmos novamente o projeto [Exemple-01], obteremos os seguintes logs:
05-28 08:37:12.709 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreate
05-28 08:37:12.778 23881-23923/com.example.android.pdfrendererbasic D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true
[ 05-28 08:37:12.781 23881:23881 D/ ]
HostConnection::get() New Host ...
05-28 08:37:12.967 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreateOptionsMenu
Assim, percebemos que o método [onCreate], que cria a atividade do Android, é executado antes do método [onCreateOptionsMenu], que cria o menu do aplicativo.
Agora, se clicarmos na opção de menu no emulador Android [1]:
![]() |
o seguinte registro é adicionado ao console de logs:
05-28 08:41:22.881 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onOptionsItemSelected
Daqui em diante, frequentemente adicionaremos instruções de log ao código Android. Na maioria das vezes, não as comentaremos. Elas estão lá apenas para incentivar o leitor a observar o console de logs, a fim de compreender gradualmente o ciclo de vida de um aplicativo Android.
1.2.2.5. Gerenciamento do emulador [Genymotion]
Às vezes, o emulador Genymotion trava e não é mais possível reiniciá-lo. Isso ocorre porque alguns processos do VirtualBox permaneceram ativos no gerenciador de tarefas. Abra o gerenciador de tarefas [Ctrl-Alt-Supp] e elimine todas as tarefas do VirtualBox presentes:
![]() | ![]() |
Feito isso, reinicie o emulador Genymotion a partir do Android Studio.
1.2.2.6. Gerenciamento do binário APK criado
A compilação do projeto gera um binário com a extensão .apk:
![]() | ![]() | ![]() |
Existem duas versões: uma chamada [debug] e outra chamada [debug-unaligned]. É preciso usar a primeira, pois a outra é uma versão intermediária. O arquivo binário .pak gerado no [4] pode ser transferido diretamente para um emulador ou dispositivo Android. Para transferi-lo para um emulador, basta arrastá-lo e soltá-lo no emulador com o mouse.
1.3. Exemplo-02: um projeto básico para Android
Vamos criar, com o Android Studio, um novo projeto Android [1-12]:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() | ![]() | ![]() |
No [13], executa-se o aplicativo. Obtém-se, então, a exibição [14] no emulador Genymotion.
1.3.1. Configuração do Gradle
O projeto criado é configurado pelo seguinte arquivo [build.gradle]:
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
}
Esse arquivo foi gerado pelo IDE com os elementos de sua configuração. Trata-se de um arquivo mínimo que iremos aprimorar gradualmente.
- linhas 3-12: as características do aplicativo Android;
- linhas 22-25: suas dependências. É principalmente aqui que faremos alterações de acordo com os exemplos estudados;
1.3.2. O manifesto do aplicativo
![]() |
O arquivo [AndroidManifest.xml] [1] define as características do binário do aplicativo Android. Seu conteúdo é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- linha 3: o pacote do projeto Android;
- linha 10: o nome da atividade;
Essas duas informações provêm dos dados inseridos durante a criação do projeto:
![]() |
- a linha 3 do manifesto (pacote) provém da entrada [4] acima. Várias classes são geradas automaticamente nesse pacote;
![]() |
- a linha 10 do manifesto (nome da atividade) provém da entrada [1] acima;
Voltemos ao manifesto:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
![]() | ![]() | ![]() |
- linha 10: a atividade principal do aplicativo. Ela faz referência à classe [1] acima;
- linha 6: o ícone [2] do aplicativo. Ele pode ser alterado;
- linha 7: o nome do aplicativo. Ele se encontra no arquivo [strings.xml] [3]:
<resources>
<string name="app_name">Exemple-02</string>
</resources>
O arquivo [strings.xml] contém as sequências de caracteres utilizadas pelo aplicativo. Na linha 2, o nome do aplicativo provém da informação inserida durante a construção do projeto [4]:
![]() |
- linha 10: uma tag de atividade. Um aplicativo Android pode ter várias atividades;
- linha 12: a atividade é designada como a atividade principal;
- linha 13: e ela deve aparecer na lista de aplicativos que podem ser iniciados no dispositivo Android.
1.3.3. A atividade principal
![]() | ![]() |
Um aplicativo Android é composto por uma ou mais atividades. Aqui, foi gerada uma atividade [1]: [MainActivity]. Uma atividade pode exibir uma ou mais visualizações, dependendo do seu tipo. A classe [MainActivity] gerada é a seguinte:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
- linha 6: a classe [MyActivity] estende a classe Android [AppCompatActivity]. Esse será o caso de todas as atividades futuras;
- linha 9: o método [onCreate] é executado quando a atividade é criada. Isso ocorre antes da exibição da vista associada à atividade;
- linha 10: o método [onCreate] da classe pai é chamado. Isso deve ser feito sempre;
- linha 11: o arquivo [activity_main.xml] [2] é a visualização associada à atividade. A definição XML dessa visualização é a seguinte:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity">
<TextView
android:text="Hello World!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- linhas b-k: o gerenciador de formatação. O escolhido por padrão é do tipo [RelativeLayout]. Nesse tipo de contêiner, os componentes são posicionados uns em relação aos outros (à direita de, à esquerda de, abaixo, acima);
- linhas m-p: um componente do tipo [TextView] que serve para exibir texto;
- linha n: o texto exibido. Não é recomendável inserir texto fixo nas visualizações. É preferível transferir esses textos para o arquivo [res/values/strings.xml] [3]:
O texto exibido será, portanto, [Hello World!]. Onde ele será exibido? O contêiner [RelativeLayout] ocupará toda a tela. O [TextView], que é seu único elemento, será exibido no canto superior esquerdo desse contêiner, portanto, no canto superior esquerdo da tela;
O que significa [R.layout.activity_main] na linha 11? Cada recurso do Android (visualizações, fragmentos, componentes, etc.) recebe um identificador. Assim, uma vista [V.xml] localizada na pasta [res / layout] será identificada por [R.layout.V]. R é uma classe gerada na pasta [app / build / generated] [1-3]:
![]() |
A classe [R] é a seguinte:
...............
public static final class string {
public static final int abc_action_bar_home_description=0x7f060000;
public static final int abc_action_bar_home_description_format=0x7f060001;
public static final int abc_action_bar_home_subtitle_description_format=0x7f060002;
...
public static final int app_name=0x7f060014;
}
public static final class layout {
public static final int abc_action_bar_title_item=0x7f040000;
public static final int abc_action_bar_up_container=0x7f040001;
...
public static final int activity_main=0x7f040019;
...
}
public static final class mipmap {
public static final int ic_launcher=0x7f030000;
}
- linha 14: o atributo [R.layout.activity_main] é o identificador da visualização [res / layout / activity_main.xml];
- linha 7: o atributo [R.string.app_name] é o identificador da sequência [app_name] no arquivo [res / values / string.xml]:
- linha 19: o atributo [R.mipmap.ic_launcher] é o identificador da imagem [res / mipmap / ic_launcher];
Portanto, é importante lembrar que, ao referenciar [R.layout.activity_main] no código, estamos nos referindo a um atributo da classe [R]. O IDE nos ajuda a identificar os diferentes elementos dessa classe:
![]() | ![]() |
1.3.4. Execução do aplicativo
Para executar um aplicativo Android, precisamos criar uma configuração de execução:
![]() | ![]() | ![]() |
- em [1], selecione [Edit Configurations];
- o projeto foi criado com uma configuração [app], que vamos excluir ([2]) para recriá-la;
- em [3], criar uma nova configuração de execução;
![]() |
- em [4], selecione [Android Application];

- em [5], na lista suspensa, selecione o módulo [app];
- em [6-8], mantenha os valores propostos por padrão;
- em [7], a atividade padrão é aquela definida no arquivo [AndroidManifest.xml] (linha 1 abaixo):
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
- em [8], selecione [Show Chooser Dialog], que permite escolher o dispositivo de execução do aplicativo (emulador, tablet);
- em [9], indique que essa escolha deve ser salva;
- confirme a configuração;
![]() |
- em [11], inicie o gerenciador de emuladores [Genymotion] (consulte o parágrafo 6.9);
![]() |
- em [12], selecione um emulador de tablet e inicie o [13];
![]() | ![]() |
- em [14], execute a configuração de execução [app];
- no [15] é exibido o formulário de seleção do dispositivo de execução. Apenas um está disponível aqui: o emulador [Genymotion] iniciado anteriormente;
Após alguns instantes, o emulador de software exibe a seguinte tela:

1.3.5. O ciclo de vida de uma atividade
Voltemos ao código da atividade [MainActivity]:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
O método [onCreate], nas linhas 8 a 12, faz parte dos métodos que podem ser chamados durante o ciclo de vida de uma atividade. A documentação do Android fornece a lista desses métodos:
![]() |
- [1]: o método [onCreate] é chamado no início da atividade. É nesse método que se associa a atividade a uma visualização e se recuperam as referências dos componentes dela;
- [2-3]: os métodos [onStart, onResume] são então chamados. Percebe-se que o método [onResume] é o último a ser executado antes de chegar ao estado [4] da atividade em execução;
1.4. Exemplo-03: reescrita do projeto [Exemple-02] com a biblioteca [Android Annotations]
Agora, vamos introduzir a biblioteca [Android Annotations], que facilita a criação de aplicativos Android. Para isso, duplicamos o exemplo [Exemple-02] em [Exemple-03], seguindo o procedimento [1-16].
![]() | ![]() |
- em [1], use a perspectiva [Project] para visualizar o projeto Android na íntegra;
![]() | ![]() |
![]() | ![]() |
![]() | ![]() | ![]() | ![]() |
Observação: entre [14] e [15], passamos de uma perspectiva [Android] para uma perspectiva [Project] (ver parágrafo 1.2.2.1).
Em seguida, modificamos o arquivo [res / values / strings.xml] [17]:
![]() |
O arquivo [strings.xml] é modificado da seguinte forma:
<resources>
<string name="app_name">Exemple-03</string>
</resources>
Agora, executamos o novo aplicativo, que herdou toda a configuração do [Exemple-02]:
![]() | ![]() |
No [19], obtemos o mesmo resultado que com o [Exemple-02], mas com um novo nome.
Vamos agora apresentar a biblioteca [Android Annotations], que, por conveniência, chamaremos de AA. Essa biblioteca introduz novas classes para anotar os códigos-fonte do Android. Essas anotações serão utilizadas por um processador que criará novas classes Java no módulo; essas classes participarão da compilação do módulo da mesma forma que as classes escritas pelo desenvolvedor. Assim, temos a seguinte cadeia de compilação:
![]() |
Primeiramente, vamos incluir no arquivo [build.gradle] as dependências do compilador de anotações AA (processador mencionado acima):
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
}
- as linhas 4-5 adicionam as duas dependências que compõem a biblioteca AA;
O arquivo [build.gradle] é modificado novamente para utilizar um plug-in chamado [android-apt], que altera o processo de compilação em duas etapas:
- processamento das anotações do Android, o que gera novas classes;
- compilação de todas as classes do projeto;
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'
- linha 8: versão do plug-in [android-apt] que será buscada no repositório central do Maven (linha 3);
- linha 13: ativação desse plugin;
Nesta etapa, verifique se a configuração de execução [app] ainda está funcionando.
Agora, vamos introduzir uma primeira anotação da biblioteca AA na classe [MainActivity]:
![]() |
A classe [MainActivity] está, por enquanto, da seguinte forma:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
Já explicamos esse código no parágrafo 1.3.3. Vamos modificá-lo da seguinte maneira:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
- linha 7: a anotação [@EActivity] é uma anotação AA (linha 3). Seu parâmetro é a visualização associada à atividade;
Essa anotação irá gerar uma classe [MainActivity_] derivada da classe [MainActivity], e é essa classe que será a verdadeira atividade. Portanto, devemos modificar o manifesto do projeto [AndroidManifest.xml] da seguinte forma:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity_">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- linha 11: a nova atividade;
Feito isso, podemos compilar o projeto [1]:
![]() | ![]() |
- no [2], vemos a classe [MainActivity_] gerada na pasta [app / build / generated / source / apt / debug];
A classe [MainActivity_] gerada é a seguinte:
//
// DO NOT EDIT THIS FILE.
// Gerado usando o AndroidAnnotations 4.0.0.
//
// Você pode criar uma obra maior que contenha este arquivo e distribuir essa obra nos termos de sua escolha.
//
package exemples.android;
import android.app.Activity;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import org.androidannotations.api.builder.ActivityIntentBuilder;
import org.androidannotations.api.builder.PostActivityStarter;
import org.androidannotations.api.view.HasViews;
import org.androidannotations.api.view.OnViewChangedNotifier;
public final class MainActivity_
extends MainActivity
implements HasViews
{
private final OnViewChangedNotifier onViewChangedNotifier_ = new OnViewChangedNotifier();
@Override
public void onCreate(Bundle savedInstanceState) {
OnViewChangedNotifier previousNotifier = OnViewChangedNotifier.replaceNotifier(onViewChangedNotifier_);
init_(savedInstanceState);
super.onCreate(savedInstanceState);
OnViewChangedNotifier.replaceNotifier(previousNotifier);
setContentView(R.layout.activity_main);
}
...
- linhas 24-25: a classe [MainActivity_] estende a classe [MainActivity];
Não tentaremos explicar o código das classes geradas pela AA. Elas lidam com a complexidade que as anotações procuram ocultar. Mas, às vezes, pode ser útil examiná-lo quando se deseja entender como as anotações que utilizamos são “traduzidas”.
Agora podemos executar novamente a configuração [app]. Obtemos o mesmo resultado de antes. A partir de agora, vamos usar esse projeto, que duplicaremos para apresentar os conceitos importantes da programação Android.
1.5. Exemplo-04: visualizações e eventos
1.5.1. Criação do projeto
Seguiremos o procedimento descrito para duplicar o [Exemple-02] no [Exemple-03] no parágrafo 1.4:
Nós:
- duplicamos o projeto [Exemple-03] em [Exemple-04] (após excluir a pasta [app / build] de [Exemple-03]);
- carregamos o projeto [Exemple-04];
- alteremos o nome do projeto no arquivo [app / res / values / strings.xml] (perspectiva Android);
- excluímos o arquivo [Exemple-04 / Exemple-04.iml] (perspectiva Projeto);
- compilemos e, em seguida, executemos o projeto;
![]() | ![]() |
1.5.2. Criando uma visualização
Agora vamos modificar, com o editor gráfico, a visualização exibida pelo projeto [Exemple-04]:
![]() | ![]() |
- no [1-4], crie uma nova visualização XML;
- em [5], nomeie a visualização;
- em [6], indique a tag raiz da visualização. Aqui, escolhemos um contêiner [RelativeLayout]. Nesse contêiner de componentes, estes são posicionados uns em relação aos outros: “à direita de”, “à esquerda de”, “abaixo de”, “acima de”;
![]() |
O arquivo [vue1.xml] gerado a partir de [7] é o 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">
</RelativeLayout>
- linha 2: um contêiner [RelativeLayout] vazio que ocupará toda a largura do tablet (linha 3) e toda a sua altura (linha 4);
![]() | ![]() |
- em [1], selecione a aba [Design] na visualização [vue1.xml] exibida;
- em [2-4], entre no modo tablet;
![]() | ![]() | ![]() |
- em [5], defina a escala do tablet como 1;
- em [6], selecione o modo “paisagem” para o tablet;
- a captura de tela [7] resume as opções selecionadas.
![]() | ![]() | ![]() |
- em [1], selecione um [Large Text] e arraste-o para a visualização [2];
- em [3], clique duas vezes no componente;
- em [4], altere o texto exibido. Em vez de inseri-lo diretamente na visualização XML, vamos externalizá-lo no arquivo [res / values / string.xml]
![]() | ![]() | ![]() |
- no [5], adicionamos um novo valor no arquivo [strings.xml];
- no [8], atribui-se um identificador à sequência;
- em [9], é fornecido o valor da cadeia de caracteres;
- em [10], a nova visualização após a validação da etapa anterior;
![]() | ![]() | ![]() |
- após clicar duas vezes no componente, altera-se seu identificador [11];
- para [12]; nas propriedades do componente, altera-se o tamanho das fontes de [50sp];
- para [13], a nova visualização;
O arquivo [vue1.xml] sofreu as seguintes alterações:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginLeft="213dp" android:layout_marginStart="213dp"
android:layout_marginTop="50dp" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:textSize="50sp"/>
</RelativeLayout>
- as alterações feitas na interface gráfica estão nas linhas 10, 11 e 14. Os demais atributos do [TextView] são valores padrão ou decorrem do posicionamento do componente na visualização;
- linhas 7-8: o tamanho do componente corresponde ao do texto que ele contém (wrap_content) em altura e largura;
- linha 13: a parte superior do componente está alinhada com a parte superior da visualização (linha 13), 50 pixels abaixo (linha 13);
- linha 12: o lado esquerdo do componente está alinhado com a esquerda da visualização (linha 13), 213 pixels à direita (linha 12);
Em geral, os tamanhos exatos das margens esquerda, direita, superior e inferior serão definidos diretamente no XML.
Seguindo o mesmo procedimento, crie a seguinte visualização [1]:
![]() |
Os componentes são os seguintes:
Posicionar os componentes uns em relação aos outros pode ser uma tarefa frustrante, já que as reações do editor gráfico às vezes são inesperadas. Pode ser preferível utilizar as propriedades dos componentes:
O componente [textView1] deve ser posicionado 50 pixels abaixo do título e a 50 pixels da borda esquerda do contêiner:
![]() | ![]() | ![]() |
- no [1], a borda superior (top) do componente está alinhada em relação à borda inferior (bottom) do componente [textViewTitreVue1] a uma distância de 50 pixels do [3] (top);
- em [2], a borda esquerda (left) do componente está alinhada em relação à borda esquerda do contêiner a uma distância de 50 pixels de [3] (left);
O componente [editTextNom] deve ser posicionado 60 pixels à direita do componente [textView1] e alinhado pela parte inferior a esse mesmo componente;
![]() | ![]() |
- no [1], a borda esquerda (left) do componente está alinhada em relação à borda direita (right) do componente [textView1] a uma distância de 60 pixels do [2] (left). Ele está alinhado à borda inferior (bottom:bottom) do componente [textView1] [1];
O componente [buttonValider] deve ser posicionado 60 pixels à direita do componente [editTextNom] e alinhado pela parte inferior a esse mesmo componente;
![]() | ![]() |
- no [1], a borda esquerda (left) do componente está alinhada em relação à borda direita (right) do componente [editTextNom] a uma distância de 60 pixels do [2] (left). Ele está alinhado à borda inferior do componente (bottom:bottom) [editTextNom] [1];
O componente [buttonVue2] deve ser posicionado 50 pixels abaixo do componente [textView1] e alinhado à esquerda com esse mesmo componente;
![]() | ![]() |
- no [1], a borda esquerda (left) do componente está alinhada em relação à borda esquerda (left) do componente [textView1] e está posicionada abaixo (top:bottom) a uma distância de 50 pixels do [2] (top);
O arquivo XML gerado é o 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="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginTop="49dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/txt_nom"
android:id="@+id/textView1"
android:layout_below="@+id/textViewTitreVue1"
android:layout_alignParentLeft="true"
android:layout_marginLeft="50dp"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextNom"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_valider"
android:id="@+id/buttonValider"
android:layout_alignBottom="@+id/editTextNom"
android:layout_toRightOf="@+id/editTextNom"
android:textSize="30sp"
android:layout_marginLeft="60dp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue2"
android:id="@+id/buttonVue2"
android:layout_below="@+id/textView1"
android:layout_alignLeft="@+id/textView1"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
</RelativeLayout>
Nele está tudo o que foi feito graficamente. Outra maneira de criar uma visualização é, portanto, editar diretamente esse arquivo. Quando se está acostumado, isso pode ser mais rápido do que usar o editor gráfico.
- Na linha 38, há uma informação que não mostramos. Ela é fornecida por meio das propriedades do componente [editTextNom] [1]:
![]() | ![]() |
Todos os textos provêm do seguinte arquivo [strings.xml] [2]:
<resources>
<string name="app_name">Exemple-04</string>
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
</resources>
Agora, vamos modificar a atividade [MainActivity] para que essa visualização seja exibida ao iniciar o aplicativo:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
- linha 7: agora é a visualização [vue1.xml] que é exibida pela atividade;
Altere o arquivo [AndroidManifest.xml] da seguinte maneira:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- linha 12: essa linha de configuração impede que o teclado apareça assim que a visualização [vue1] é exibida. Na verdade, essa visualização possui um campo de entrada que recebe o foco ao ser exibida. Esse foco faz com que o teclado virtual apareça por padrão;
Execute o aplicativo e verifique se é realmente a visualização [vue1.xml] que está sendo exibida:

1.5.3. Gerenciamento de eventos
Vamos agora gerenciar o clique no botão [Valider] da visualização [Vue1]:

O código de [MainActivity] passa a ser o seguinte:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
// os elementos da interface visual
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews(){
Log.d("MainActivity","afterViews");
}
// gerenciador de eventos
@Click(R.id.buttonValider)
protected void doValider() {
// é exibido o nome digitado
Toast.makeText(this, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- linhas 17-18: associa-se o campo [protected EditText editTextNom] ao componente de identificador [R.id.editTextNom] da interface visual. O campo associado ao componente deve estar acessível na classe derivada [MainActivity_] e, por esse motivo, não pode ter o escopo [private]. O campo identificado por [R.id.editTextNom] provém da visualização [vue1.xml]:
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextNom"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
Observação: não utilize caracteres acentuados nos identificadores [id]. O AA não os processa corretamente.
- linha 32: a anotação [@Click(R.id.buttonValider)] indica o método que lida com o evento 'Click' no botão com o identificador [R.id.buttonValider]. Esse identificador também provém da visualização [vue1.xml]:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_valider"
android:id="@+id/buttonValider"
android:layout_alignBottom="@+id/editTextNom"
android:layout_toRightOf="@+id/editTextNom"
android:textSize="30sp"
android:layout_marginLeft="60dp"/>
- linha 35: exibe o nome digitado:
- Toast.makeText(...).show(): exibe um texto na tela,
- o primeiro parâmetro de makeText é a atividade,
- o segundo parâmetro é o texto a ser exibido na caixa que será exibida por makeText,
- o terceiro parâmetro é o tempo de exibição da caixa: Toast.LENGTH_LONG ou Toast.LENGTH_SHORT;
- na linha 26, a anotação [@AfterViews] indica o método a ser executado quando todos os campos anotados por [@ViewById] forem inicializados. É importante saber quando esses campos são inicializados. Por exemplo, no método [onCreate], é possível usar a referência da linha 18? Para responder a essa pergunta, inserimos registros de log;
Execute o projeto [Exemple-04] e verifique se algo acontece quando você clica no botão [Valider]. Obtemos os seguintes logs:
Concluímos que, quando o método [onCreate] é executado, os campos anotados por [@ViewById] ainda não estão inicializados. Mais uma vez, o leitor iniciante é incentivado a inserir esse tipo de log nos métodos que gerenciam o ciclo de vida do aplicativo.
1.6. Exemplo-05: navegação entre visualizações
No projeto anterior, o botão [Vue n° 2] não foi utilizado. Propomos utilizá-lo criando uma segunda visualização e mostrando como navegar de uma visualização para outra. Existem várias maneiras de resolver esse problema. A proposta aqui é associar cada visualização a uma atividade. Outro método é ter uma única atividade do tipo [AppCompatActivity] que exiba visualizações do tipo [Fragment]. Esse será o método utilizado em aplicativos futuros.
1.6.1. Criação do projeto
Duplicamos o projeto [Exemple-04] em [Exemple-05]. Para isso, seguiremos o procedimento descrito para duplicar o [Exemple-02] no [Exemple-03] no parágrafo 1.4 e que foi reproduzido no parágrafo 1.5.
![]() | ![]() |
1.6.2. Adição de uma segunda atividade
Para gerenciar uma segunda visualização, vamos criar uma segunda atividade. É ela que gerenciará a visualização nº 2. Estamos aqui no modelo “uma visualização = uma atividade”. Existem outros modelos possíveis.
123

- em [1-4], criamos uma nova atividade;

- em [5], o nome da classe que será gerada;
- em [6], o nome da visualização (vue2.xml) associada à nova atividade;
![]() |
- em [7-8], os arquivos afetados pela configuração anterior;
A atividade [SecondActivity] é a seguinte:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.vue2);
}
}
- linha 11: a atividade está associada à visualização [vue2.xml];
A visualização [vue2.xml] é a seguinte:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.SecondActivity">
</RelativeLayout>
Trata-se de uma visualização, por enquanto vazia, com um gerenciador de layout do tipo [RelativeLayout] (linha 2). Na linha 11, vemos que ela foi associada à nova atividade.
O manifesto do módulo Android [AndroidManifest.xml] sofreu as seguintes alterações:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity">
</activity>
</application>
</manifest>
Na linha 20, uma segunda atividade foi registrada.
1.6.3. Navegação da visualização nº 1 para a visualização nº 2
Voltemos ao código da classe [MainActivity], que exibe a visualização nº 1. A transição para a visualização nº 2 não está sendo gerenciada no momento:
![]() |
Nós a implementamos da seguinte maneira:
// navegar para a visualização nº 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
// navega-se para a visualização nº 2, passando a ela o nome inserido na visualização nº 1
// cria-se um Intent
Intent intent = new Intent();
// associa-se esse Intent a uma atividade
intent.setClass(this, SecondActivity.class);
// associamos informações a esse Intent
intent.putExtra("NOM", editTextNom.getText().toString().trim());
// inicia-se a atividade do tipo [SecondActivity], passando-lhe o Intent
startActivity(intent);
}
- linhas 2-3: o método [navigateToView2] lida com o “clique” no botão identificado por [R.id.buttonVue2], definido na visualização [vue1.xml]:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue2"
android:id="@+id/buttonVue2"
android:layout_below="@+id/textView1"
android:layout_alignLeft="@+id/textView1"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
Os comentários descrevem as etapas a serem realizadas para a mudança de visualização:
- linha 6: criar um objeto do tipo [Intent]. Esse objeto permitirá especificar tanto a atividade a ser iniciada quanto as informações a serem passadas para ela;
- linha 8: associar o Intent a uma atividade, neste caso uma atividade do tipo [SecondActivity], que será responsável por exibir a visualização nº 2. É importante lembrar que a atividade [MainActivity] exibe a visualização nº 1. Portanto, temos uma visualização = uma atividade. Precisaremos definir o tipo [SecondActivity];
- linha 10: opcionalmente, inserir informações no objeto [Intent]. Essas informações destinam-se à atividade [SecondActivity], que será iniciada. Os parâmetros de [Intent.putExtra] são (Objeto-chave, Objeto-valor). Observe-se que o método [EditText.getText()], que retorna o texto digitado no campo de entrada, não retorna um tipo [String], mas sim um tipo [Editable]. É necessário utilizar o método [toString] para obter o texto digitado;
- linha 12: execute a atividade definida pelo objeto [Intent].
Execute o projeto [Exemple-05] e verifique se a visualização nº 2 (vazia por enquanto) é exibida corretamente:
![]() | ![]() |
1.6.4. Criação da visualização nº 2
![]() | ![]() |
- No [1-2], excluímos a visão [main.xml], que não nos serve mais, e, em seguida, modificamos a visão [vue2.xml] da seguinte maneira:
![]() |
Os componentes são os seguintes:
O arquivo XML [vue2.xml] é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.SecondActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue2"
android:id="@+id/textViewTitreVue2"
android:layout_marginTop="50dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textViewBonjour"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_below="@+id/textViewTitreVue2"
android:layout_marginTop="50dp"
android:layout_marginLeft="50dp"
android:textSize="30sp"
android:text="Bonjour !"
android:textColor="#ffffb91b"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue1"
android:id="@+id/buttonVue1"
android:layout_marginTop="50dp"
android:textSize="30sp"
android:layout_alignLeft="@+id/textViewBonjour"
android:layout_below="@+id/textViewBonjour"/>
</RelativeLayout>
Execute o projeto [Exemple-05] e verifique se a nova visualização é exibida ao clicar no botão [Vue n° 2].
1.6.5. A atividade [SecondActivity]
No [MainActivity], escrevemos o seguinte código:
// navegar para a visualização nº 2
protected void navigateToView2() {
// navega-se para a visualização nº 2, passando a ela o nome inserido na visualização nº 1
// cria-se um Intent
Intent intent = new Intent();
// associa-se esse Intent a uma atividade
intent.setClass(this, SecondActivity.class);
// associamos informações a esse Intent
intent.putExtra("NOM", edtNom.getText().toString().trim());
// inicia-se a atividade do tipo [SecondActivity], passando-lhe o Intent
startActivity(intent);
}
Na linha 9, inserimos informações para [SecondActivity] que não foram utilizadas. Agora vamos utilizá-las, e isso ocorre no código de [SecondActivity]:
![]() |
O código de [SecondActivity] sofre as seguintes alterações:
package exemples.android;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
// componentes da interface visual
@ViewById
protected TextView textViewBonjour;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews() {
// recupera-se o Intent, caso exista
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// recupera-se o nome
String nom = extras.getString("NOM");
if (nom != null) {
// exibimos o nome
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
}
}
}
- linha 11: utiliza-se a anotação [@EActivity] para indicar que a classe [SecondActivity] é uma atividade associada à visualização [vue2.xml];
- linhas 15-16: obtém-se uma referência ao componente [TextView] identificado por [R.id.textViewBonjour]. Aqui, não foi escrito [@ViewById(R.id.textViewBonjour)]. Nesse caso, AA pressupõe que o identificador do componente seja idêntico ao campo anotado, neste caso, o campo [textViewBonjour];
- linha 23: a anotação [@AfterViews] anota um método que deve ser executado após a inicialização dos campos anotados por [@ViewById]. No método [OnCreate] (linha 19), não é possível utilizar esses campos, pois eles ainda não foram inicializados. No projeto [Exemple-05], há uma transição de uma atividade para outra e, a princípio, não estava claro se o método anotado [@AfterViews] seria executado uma vez na instanciação inicial da atividade ou sempre que a atividade fosse iniciada. Os testes mostraram que a segunda hipótese estava correta;
- linha 26: a classe [AppCompatActivity] possui um método [getIntent] que retorna o objeto [Intent] associado à atividade;
- linha 28: o método [Intent.getExtras] retorna um tipo [Bundle], que é uma espécie de dicionário contendo as informações associadas ao objeto [Intent] da atividade;
- linha 31: recupera-se o nome armazenado no objeto [Intent] da atividade;
- linha 34: ele é exibido.
Lembrete: os campos marcados com a anotação [@ViewById] não devem conter caracteres acentuados.
Voltemos à classe [SecondActivity]. Como escrevemos:
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
AA irá gerar uma classe [SecondActivity_] derivada de [SecondActivity], e é essa classe que será a verdadeira atividade. Isso nos leva a fazer alterações em:
[MainActivity]
// navegar para a visualização nº 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
..
// associa-se esse Intent a uma atividade
intent.setClass(this, SecondActivity_.class);
...
}
- na linha 6, devemos substituir [SecondActivity] por [SecondActivity_];
[AndroidManifest.xml]
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity_">
</activity>
</application>
</manifest>
- na linha 20, é preciso substituir [SecondActivity] por [SecondActivity_];
Teste esta nova versão. Digite um nome na visualização nº 1 e verifique se a visualização nº 2 o exibe corretamente.
![]() | ![]() |
1.6.6. Navegação da visualização nº 2 para a visualização nº 1
Para navegar da visualização nº 2 para a visualização nº 1, seguiremos o procedimento visto anteriormente:
- inserir o código de navegação na atividade [SecondActivity], que exibe a visualização nº 2;
- escrever o método [@AfterViews] na atividade [MainActivity], que exibe a visualização nº 1;
O código de [SecondActivity] passa a ser o seguinte:
@Click(R.id.buttonVue1)
protected void navigateToView1() {
// criamos um Intent para a atividade [MainActivity]
Intent intent1 = new Intent();
intent1.setClass(this, MainActivity_.class);
// recuperar o Intent da atividade atual [SecondActivity]
Intent intent2 = getIntent();
if (intent2 != null) {
Bundle extras2 = intent2.getExtras();
if (extras2 != null) {
// insere-se o nome no Intent de [MainActivity]
intent1.putExtra("NOM", extras2.getString("NOM"));
}
// inicia-se [MainActivity]
startActivity(intent1);
}
}
- linhas 1-2: associa-se o método [navigateToView1] ao clique no botão [btn_vue1];
- linha 4: cria-se um novo [Intent];
- linha 5: associada à atividade [MainActivity_];
- linha 7: recupera-se o Intent associado a [SecondActivity];
- linha 9: recuperam-se as informações desse Intent;
- linha 12: a chave [NOM] é recuperada de [intent2] para ser inserida em [intent1] com o mesmo valor associado;
- linha 15: a atividade [MainActivity_] é iniciada.
No código de [MainActivity], adiciona-se o seguinte método [@AfterViews]:
@AfterViews
protected void afterViews() {
// recupera-se o Intent, caso exista
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// recupera-se o nome
String nom = extras.getString("NOM");
if (nom != null) {
// exibe-se
editTextNom.setText(nom);
}
}
}
}
Faça essas alterações e teste sua aplicação. Agora, ao voltar da visualização nº 2 para a visualização nº 1, deve-se encontrar o nome inserido inicialmente, o que não acontecia até agora.
![]() | ![]() |
1.6.7. Ciclo de vida das atividades
Apresentamos, no parágrafo 1.3.5, o ciclo de vida de uma atividade. Temos aqui duas atividades e alternamos entre elas durante a execução. Essas atividades contêm dois métodos, e não sabemos ao certo quando um é chamado em relação ao outro: [onCreate] e [afterViews]. É importante saber isso. Para isso, adicionamos registros de log nas duas atividades:
Assim, na classe [MainActivity], escrevemos:
// fabricante
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity", "onCreate");
...
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
}
}
- linhas 2-4: queremos saber se a classe [MainActivity] é instanciada uma ou mais vezes;
- linha 8: queremos saber se o método [onCreate] é chamado uma ou mais vezes;
- linha 14: queremos saber se o método [afterViews] é chamado uma ou mais vezes;
Fazemos exatamente o mesmo na classe [SecondActivity].
Ao iniciar o aplicativo, temos os seguintes logs:
Os métodos [onCreate, afterViews] da primeira atividade foram executados nessa ordem. Ao clicar no botão [Vue n° 2], os novos logs são os seguintes:
Os métodos [onCreate, afterViews] da segunda atividade foram executados nessa ordem. Ao clicar no botão [Vue n° 1], os novos logs são os seguintes:
A classe [MainActivity] é, portanto, instanciada novamente. Ao clicar no botão [Vue n° 2], os novos logs são os seguintes:
A classe [SecondActivity] é, portanto, instanciada novamente.
As duas atividades são, portanto, recriadas sistematicamente sempre que se muda de atividade.
Vamos agora conhecer uma arquitetura com uma única atividade capaz de gerenciar várias visualizações chamadas fragmentos. A atividade e as visualizações serão instanciadas apenas uma vez, ao contrário do método anterior, em que uma atividade podia ser instanciada várias vezes.
1.7. Exemplo-06: navegação por abas
Vamos explorar aqui as interfaces com abas. O exemplo é complexo, mas apresenta todos os elementos que utilizaremos posteriormente: atividade única, gerenciador de fragmentos (visões), contêiner de fragmentos e navegação entre fragmentos. O conceito de abas é diferente do de fragmentos e tem importância secundária no que pretendemos demonstrar neste exemplo.
1.7.1. Criação do projeto
Criamos um novo projeto:
![]() | ![]() |
![]() |
![]() |
- no [7], escolhemos uma atividade com abas (Tabbed Activity);
![]() |
- em [10-14], mantêm-se os valores propostos por padrão;
- em [15], seleciona-se abas com uma barra de títulos;
O projeto criado fica então da seguinte forma:
![]() | ![]() |
- no [1], a atividade;
- em [2], as visualizações;
Uma configuração de execução [app], com o nome do módulo, foi criada automaticamente [2b]:
![]() |
É possível executá-la. Em seguida, é exibida uma janela com três guias [3-6]:

1.7.2. Configuração do Gradle
O projeto [Exemple-06] foi gerado com o seguinte arquivo [build.gradle]:
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
}
Há uma novidade em relação ao que já foi visto: a linha 25. Essa biblioteca é necessária para os novos componentes utilizados pelo aplicativo gerado.
1.7.3. A visualização [activity_main]
![]() |
A visualização [activity_main] é a visualização associada à atividade [MainActivity] do projeto. No modo [design], a visualização é a seguinte:

Ela contém os seguintes componentes:
![]() |
- [main_content] é a visão completa;
- [appbar] (quadro vermelho, 1) é a barra de aplicativos. Ela contém dois componentes:
- [toolbar] (quadro amarelo 4) é a barra de ferramentas;
- [tabs] (quadro laranja, 5) é a barra de título das abas;
- [container] (quadro verde, 2) pode acomodar diversos fragmentos. Um fragmento é uma visualização. Assim, a mesma atividade poderá exibir várias visualizações (fragmentos) nesse contêiner;
- [fab] (componente 3) é chamado de componente flutuante;
No modo [text], o código é o 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="exemples.android.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
Encontramos aqui os elementos descritos anteriormente:
- linhas 2-49: a definição do componente [main_content] (linha 5), que corresponde à totalidade da visualização. Vemos que se trata de um layout (gerenciador de disposição de componentes) do tipo [CoordinatorLayout] (linha 2);
- linhas 11-33: o contêiner [appbar] (linha 12). Trata-se de um layout do tipo [AppBarLayout] (linha 11);
- linhas 18-24: o componente [toolbar] (linha 19) do tipo [Toolbar] (linha 18);
- linhas 28-31: o contêiner [tabs] (linha 29). Trata-se de um layout do tipo [TabLayout] (linha 28). Ele exibirá os títulos das abas;
- linhas 35-39: o componente [container] (linha 36). É esse contêiner que exibe as diferentes visualizações da atividade;
- linhas 41-47: o componente [fab] (linha 42) do tipo [FloatingActionButton] (linha 41). Trata-se de um botão no qual é possível clicar. Por padrão, ele fica localizado no canto inferior direito da visualização geral;
Não tentaremos entender o significado de todos os atributos desses componentes. Vamos utilizá-los tal como estão. É com a experiência e, muitas vezes, no modo [design] que se descobre sua função. Nesse modo, percebe-se que os componentes possuem dezenas de atributos. Em geral, apenas alguns são inicializados, enquanto os demais mantêm um valor padrão.
No entanto, vamos esclarecer alguns pontos. A maioria dos valores que configuram as diferentes visualizações está reunida na pasta [res / values]:
![]() |
Esses valores são referenciados nas linhas 15-16, 23, 39 e 46 do arquivo [activity_main.xml]. Vejamos um exemplo:
- linha 15:
android:paddingTop="@dimen/appbar_padding_top"
A anotação [@dimen] faz referência ao arquivo [res / values / dimens.xml]:
<resources>
<!-- Margens padrão da tela, de acordo com as diretrizes de design do Android. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="appbar_padding_top">8dp</dimen>
</resources>
A linha 15 do arquivo [activity_main.xml] faz referência à linha (f) acima;
Da mesma forma, a anotação:
- [@string] refere-se ao arquivo de recursos [res / values / strings.xml];
- [@color] refere-se ao arquivo de recursos [res / values / colors.xml];
- [@style] refere-se ao arquivo de recursos [res / values / styles.xml];
1.7.4. A atividade
![]() |
O código gerado para a atividade está à altura da visão descrita anteriormente: é complexo. Vamos analisá-lo em várias etapas.
1.7.4.1. Gerenciamento de fragmentos e abas
O código de [MainActivity] referente aos fragmentos e guias é o seguinte:
package exemples.android;
import android.support.design.widget.TabLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// o gerenciador de fragmentos
private SectionsPagerAdapter mSectionsPagerAdapter;
// o contêiner de fragmentos
private ViewPager mViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
// pai
super.onCreate(savedInstanceState);
// visualização
setContentView(R.layout.activity_main);
// barra de ferramentas
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// o 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 = (ViewPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// a barra de abas também está associada ao contêiner de fragmentos
// ou seja, a aba nº i exibe o fragmento nº i do contêiner
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
}
// um fragmento
public static class PlaceholderFragment extends Fragment {
...
}
// o gerenciador de fragmentos
// é a ele que se solicita os fragmentos a serem exibidos na visualização principal
// deve definir os métodos [getItem] e [getCount] — os demais são opcionais
public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
}
}
- linha 28: o Android fornece um contêiner de visualizações do tipo [android.support.v4.view.ViewPager] (linha 12). É necessário fornecer a esse contêiner um gerenciador de visualizações ou fragmentos. É o desenvolvedor quem o fornece;
- linha 25: o gerenciador de fragmentos utilizado neste exemplo. Sua implementação está nas linhas 61-63;
- linha 31: o método executado na criação da atividade;
- linha 35: a visualização [activity_main.xml] é associada à atividade;
- linha 37: obtém-se a referência do componente [toolbar] da vista por meio de seu identificador;
- linha 38: essa barra de ferramentas se torna a barra de ação (um conceito do Android) da atividade;
- linha 40: o gerenciador de fragmentos é instanciado. O parâmetro do construtor é a classe Android [android.support.v4.app.FragmentManager] (linha 10);
- linha 44: na vista [activity_main.xml], obtém-se a referência do contêiner de fragmentos por meio de seu identificador;
- linha 45: o gerenciador de fragmentos é vinculado ao contêiner de fragmentos. Isso significa que, quando for solicitado ao contêiner de fragmentos que exiba o fragmento nº i, este será solicitado ao gerenciador de fragmentos;
- linha 48: obtém-se uma referência à barra de abas por meio de seu identificador;
- linha 49: o gerenciador de abas está associado ao contêiner de fragmentos. Isso significa que, quando clicarmos na aba nº i, o contêiner exibirá o fragmento nº i. A associação feita entre o gerenciador de abas e o contêiner de fragmentos nos poupa de qualquer gerenciamento das abas. Assim, não precisamos definir um gerenciador de eventos para o clique em uma aba. A associação com o contêiner de fragmentos já fornece isso por padrão. Veremos um exemplo em que haverá mais fragmentos do que abas. Nesse caso, não fazemos essa associação.
O gerenciador de fragmentos [SectionsPagerAdapter] é o seguinte:
// o gerenciador de fragmentos
// é a ele que se solicita os fragmentos a serem exibidos na visualização principal
// deve definir os métodos [getItem] e [getCount] — os demais são opcionais
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// fragmento n.º posição
@Override
public Fragment getItem(int position) {
// instancia-se um fragmento [PlaceHolder] e ele é renderizado
return PlaceholderFragment.newInstance(position + 1);
}
// retorna o número de fragmentos gerenciados
@Override
public int getCount() {
return 3;
}
// opcional — atribui um título aos fragmentos gerenciados
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return "SECTION 1";
case 1:
return "SECTION 2";
case 2:
return "SECTION 3";
}
return null;
}
}
}
- os fragmentos exibidos por um aplicativo dependem dele. O gerenciador de fragmentos é definido pelo desenvolvedor;
- linha 5: o gerenciador de fragmentos estende a classe Android [android.support.v4.app.FragmentPagerAdapter]. O construtor é imposto. Precisamos definir pelo menos os dois métodos a seguir:
- int getCount(): retorna o número de fragmentos a serem gerenciados;
- Fragment getItem(i): retorna o fragmento nº i;
O método CharSequence getPageTitle(i), que gera o título do fragmento nº i, é opcional. Como o gerenciador de abas foi associado ao gerenciador de fragmentos, o título da aba nº i será o título do fragmento nº i. Assim, os títulos das linhas 27 a 33 serão os títulos das abas;
- linhas 18-21: getCount indica o número de fragmentos gerenciados, neste caso, três;
- linhas 11-15: getItem(i) retorna o fragmento nº i. Aqui, todos os fragmentos serão idênticos, do tipo [PlaceholderFragment];
- linhas 24-35: getPageTitle(int i) retorna o título do fragmento nº i;
1.7.4.2. Os fragmentos exibidos
![]() |
Os fragmentos da atividade têm, neste caso, todos o mesmo tipo e estão todos associados à seguinte visualização XML [fragment_main]:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- linhas 1-16: um layout do tipo [RelativeLayout];
- linhas 11-14: o único componente da visualização (fragmento): um [TextView] identificado por [section_label];
No [MainActivity], os fragmentos gerenciados são do tipo [PlaceholderFragment], conforme segue:
// um fragmento
public static class PlaceholderFragment extends Fragment {
// um texto exibido no fragmento
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
}
// retorna um fragmento com uma informação: o número do fragmento passado como parâmetro
public static PlaceholderFragment newInstance(int sectionNumber) {
// fragmento
PlaceholderFragment fragment = new PlaceholderFragment();
// informação incorporada
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// resultado
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// a visualização [fragment_main] é instanciada
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
// o [TextView] foi encontrado
TextView textView = (TextView) rootView.findViewById(R.id.section_label);
// seu conteúdo é modificado
textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
// a visualização é retornada
return rootView;
}
}
- linha 2: a classe [PlaceholderFragment] estende a classe Android [Fragment]. Geralmente, isso sempre ocorre;
- linha 2: a classe [PlaceholderFragment] é estática. Seu método [newInstance] (linha 10) permite obter instâncias do tipo [PlaceholderFragment];
- linhas 10-19: o método [newInstance] cria e retorna um objeto do tipo [PlaceholderFragment];
- linhas 14-16: o fragmento é criado com um argumento;
Um fragmento deve definir o método [onCreateView] da linha 22. Esse método deve retornar a visualização associada ao fragmento.
- linha 25: a visualização [fragment_main.xml] é associada ao fragmento;
- linha 27: essa visualização contém um componente [TextView], cuja referência é obtida por meio de seu identificador;
- linha 29: exibe-se um texto no [TextView];
- [getString] é um método da classe pai [AppCompatActivity];
- o primeiro argumento é um número de componente. [R.string.section_format] designa o número do componente identificado por [section_format] no arquivo [res / values / strings.xml] (linha 4 abaixo):
<resources>
<string name="app_name">Exemple-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
- (continuação)
- a linha (d) acima %1$d indica que o argumento nº 1 (%1) deve ser formatado como um número inteiro ($d);
- o segundo argumento de [getString] é o valor a ser atribuído ao argumento $1 da linha (d) acima;
- [getArguments] fornece a referência do pacote de argumentos do fragmento. É importante lembrar aqui que cada argumento foi criado com o seguinte pacote (linhas f-h):
// retorna um fragmento com uma informação: o número do fragmento passado como parâmetro
public static PlaceholderFragment newInstance(int sectionNumber) {
// fragmento
PlaceholderFragment fragment = new PlaceholderFragment();
// informação incorporada
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// resultado
return fragment;
}
- (continuação)
- getArguments().getInt(ARG_SECTION_NUMBER) retornará, portanto, o valor [sectionNumber] das linhas (g) e (b) acima;
- linha 31: exibe-se a visualização assim criada;
1.7.4.3. Gerenciamento do menu
No aplicativo gerado, há um menu:
![]() |
O conteúdo do arquivo [menu_main.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="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- linhas 1-9: o menu;
- linhas 5-8: um item do menu identificado por [action_settings] (linha 5);
- linha 6: o rótulo da opção do menu. Ele se encontra no arquivo [res / values / strings.xml] (linha (c) abaixo):
<resources>
<string name="app_name">Exemple-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
O código anterior corresponde ao seguinte elemento visual (o menu fica no canto superior direito da janela de execução do Android):
![]() | ![]() |
Esse menu é gerenciado da seguinte maneira na atividade [MainActivity]:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Expande o menu; isso adiciona itens à barra de ação, caso ela esteja presente.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Trate aqui os cliques nos itens da barra de ação. A barra de ação irá
// processará automaticamente os cliques no botão Início/Para cima, desde que
// você especificar uma atividade pai em AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
- linhas 1-6: esse método é chamado quando o sistema está pronto para criar o menu do aplicativo. O parâmetro de entrada [Menu menu] é um menu vazio que ainda não possui opções;
- linha 4: o arquivo [res / menu / menu_main.xml] é processado. Ao objeto [Menu menu], passado como parâmetro, são atribuídas as opções de menu definidas nesse arquivo;
- linha 5: indica-se que a criação do menu foi concluída;
- linhas 8-21: o método [onOptionsItemSelected] é executado assim que uma opção do menu é clicada;
- linha 13: a referência da opção do menu clicada;
- linhas 16-18: se a opção clicada for a opção com o identificador [action_settings], nada é feito e indica-se que o evento foi processado (linha 17);
- linha 20: o evento é repassado para a classe pai;
Para entender melhor o que acontece com esse menu, adicionamos registros de log ao código anterior:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("menu", "création menu en cours");
// Inflate o menu; isso adiciona itens à barra de ação, caso ela esteja presente.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// Trate aqui os cliques nos itens da barra de ação. A barra de ação irá
// processará automaticamente os cliques no botão Início/Para cima, desde que
// você especificar uma atividade pai em AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
// pai
return super.onOptionsItemSelected(item);
}
1.7.4.4. O botão flutuante
A visualização gerada possui um botão flutuante:
![]() |
Esse componente está definido na visualização principal [activity-main.xml]:
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
A linha 7 faz referência a uma imagem fornecida pelo suporte do Android, a de um envelope.
Esse componente é gerenciado na classe [MainActivity] da seguinte maneira:
// botão flutuante
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
- linha 2: recupera-se a referência do botão flutuante na visualização associada à atividade (activity_main);
- linhas 3-9: associa-se a ele um manipulador para lidar com o clique nele;
- linha 6: a classe [Snackbar] permite exibir mensagens temporárias na visualização por meio de seu método [Snackbar.make]. O primeiro argumento é uma visualização a partir da qual [Snackbar] buscará uma visualização pai na qual exibir a mensagem. Aqui, [view] é a visualização do envelope clicado (linha 5). A vista pai que será encontrada será a vista [activity_main]. O segundo argumento é a mensagem a ser exibida. O terceiro argumento é a duração da exibição (SHORT ou LONG);
- linha 7: é possível clicar na mensagem exibida e, assim, acionar uma ação. Aqui, nenhuma ação está associada ao clique na mensagem. Por fim, o método [show] exibe a mensagem;
O clique no botão flutuante produz o seguinte resultado visual:
![]() |
1.7.5. Execução do projeto
Agora que explicamos os detalhes do código gerado, podemos compreender melhor sua execução:

Ao clicar na aba nº i, o fragmento nº i é exibido no contêiner de visualizações. Isso fica evidente pelo texto exibido em [4]. Também é possível notar que se pode alternar entre as abas arrastando a visualização para a direita ou para a esquerda com o mouse (deslizar). Veremos que é possível controlar esse comportamento.
Ao clicar na opção de menu em [6], obtêm-se os seguintes logs:
![]() |
1.7.6. Ciclo de vida dos fragmentos
![]() | ![]() |
- em [1], percebe-se que o método [onCreateView] e os seguintes são executados na primeira exibição do fragmento e sempre que a atividade precisar reexibi-lo;
Para acompanhar o ciclo de vida da atividade e dos fragmentos, adicionamos os seguintes logs no código de [MainActivity]:
// construtor
public MainActivity(){
Log.d("MainActivity","constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
// pai
super.onCreate(savedInstanceState);
...
}
// um fragmento
public static class PlaceholderFragment extends Fragment {
// um texto exibido no fragmento
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
// retorna um fragmento com uma informação: o número do fragmento passado como parâmetro
public static PlaceholderFragment newInstance(int sectionNumber) {
Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
// fragmento
PlaceholderFragment fragment = new PlaceholderFragment();
...
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("PlaceholderFragment", String.format("newInstance %s", getArguments().getInt(ARG_SECTION_NUMBER)));
...
}
}
}
Executamos o projeto novamente. Os primeiros logs são os seguintes:
- linha 1: criação da atividade;
- linha 2: execução do método [onCreate];
- linhas 3-4: instanciação do fragmento nº 1;
- linhas 5-6: instanciação do fragmento nº 2;
- linha 7: inicialização do fragmento nº 2;
- linha 8: inicialização do fragmento nº 1;
- linha 9: criação do menu da atividade;
É importante lembrar aqui o código responsável pela criação dos fragmentos:
// o gerenciador de fragmentos
// é a ele que se solicita os fragmentos a serem exibidos na visualização principal
// deve definir os métodos [getItem] e [getCount] — os demais são opcionais
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// fragmento n.º posição
@Override
public Fragment getItem(int position) {
// instancia-se um fragmento [PlaceHolder] e ele é renderizado
return PlaceholderFragment.newInstance(position + 1);
}
...
- linhas 11-15: um fragmento é instanciado por [newInstance] sempre que o contêiner de fragmentos solicita um;
Os logs acima mostram que os dois primeiros fragmentos foram instanciados e inicializados.
Agora, vamos clicar na aba nº 2. Os novos logs são os seguintes:
- linhas 1-3: o fragmento nº 3 é instanciado e inicializado. Vale lembrar que é o fragmento nº 2 que está sendo exibido;
Agora, vamos clicar na aba nº 3. Lá não há nenhum registro. Provavelmente porque o fragmento nº 3 a ser exibido já havia sido instanciado. Agora, vamos voltar à aba nº 1. Os registros são então os seguintes:
O fragmento nº 1 não é instanciado novamente, mas seu método [onCreateView] é executado novamente. Esse comportamento se repete para os outros dois fragmentos.
A partir desses logs, podemos concluir que:
- a atividade foi instanciada e, em seguida, inicializada uma vez;
- que cada fragmento foi instanciado uma vez;
- que o método [onCreateView] de cada fragmento foi executado várias vezes;
O que é preciso saber e o que os logs confirmam é que, por padrão, quando um fragmento nº i é exibido, os fragmentos i-1 e i+1 são instanciados, caso ainda não o tenham sido. É isso que explica, por exemplo, que, no início, quando é necessário exibir o fragmento nº 1, são os fragmentos 1 e 2 que foram instanciados e inicializados. O que os logs também mostram é que o método [getItem(i)] é chamado apenas uma vez, mesmo que o fragmento nº i seja exibido várias vezes. Assim, parece que o contêiner de fragmentos [ViewPager], que deve exibir o fragmento nº i, solicita esse fragmento uma vez ao gerenciador de fragmentos [SectionsPagerAdapter]. Depois disso, ele não o solicita novamente e continua a utilizar o que obteve.
Por fim, os logs fornecem informações sobre o método [onCreateView] dos fragmentos:
- no início, os fragmentos 1 e 2 foram instanciados e seu método [onCreateView] foi executado;
- ao passar do fragmento 1 para o fragmento 2, o método [onCreateView] do fragmento 2 não é reexecutado. Portanto, não é possível utilizá-lo para atualizar o fragmento 2. No entanto, o usuário pode, com o fragmento 1, ter realizado uma operação cujo resultado deveria ser exibido pelo fragmento 2. Percebe-se que o método [onCreateView] não poderá ser utilizado para atualizar o fragmento 2. Será necessário encontrar outra solução;
1.8. Exemplo-07: Exemplo-06 reescrito com a biblioteca [AA]
1.8.1. Criação do projeto
Vamos duplicar o projeto [Exemple-06] em [Exemple-07] para inserir neste último as anotações do Android. Para isso, siga o procedimento descrito no parágrafo 1.4. Obtemos o seguinte resultado:
![]() | ![]() |
1.8.2. Configuração do Gradle
![]() |
Alteramos o arquivo [build.gradle] da seguinte maneira:
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 {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
Adicionamos a configuração necessária para utilizar a biblioteca [Android Annotations] (ver parágrafo 1.4).
1.8.3. Adição das primeiras anotações AA
Vamos criar anotações AA no arquivo [MainActivity]:
![]() |
A classe [MainActivity] sofre as seguintes alterações:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// o gerenciador de fragmentos
private SectionsPagerAdapter mSectionsPagerAdapter;
// o contêiner de fragmentos
@ViewById(R.id.container)
protected MyPager mViewPager;
// o gerenciador de abas
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// o botão flutuante
@ViewById(R.id.fab)
protected FloatingActionButton fab;
// construtor
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
// barra de ferramentas
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// o 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.setAdapter(mSectionsPagerAdapter);
// a barra de abas também está associada ao contêiner de fragmentos
// ou seja, a aba nº i exibe o fragmento nº i do contêiner
tabLayout.setupWithViewPager(mViewPager);
// botão flutuante
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
- linha 1: a anotação [@EActivity] transforma [MainActivity] em uma classe gerenciada por AA. Seu parâmetro [R.layout.activity_main] é o identificador da visualização [activity_main.xml] associada à atividade;
- linhas 11-12: o componente identificado por [R.id.tabs] é injetado no campo [tabLayout]. Trata-se do gerenciador de abas;
- linhas 14-15: o componente identificado por [R.id.fab] é inserido no campo [fab]. Trata-se do botão flutuante;
- linhas 23-50: o código que antes estava no método [onCreate] é migrado para um método com nome qualquer, mas anotado por [@AfterViews] (linha 23). No método assim anotado, temos a garantia de que todos os componentes da interface visual anotados por [@ViewById] foram inicializados;
- além disso, foram inseridos logs para acompanhar o ciclo de vida da atividade;
Lembre-se de que a anotação [@EActivity] irá gerar uma classe [MainActivity_], que será a verdadeira atividade do projeto. Portanto, é necessário modificar o arquivo [AndroidManifest.xml] da seguinte maneira:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- linha 12: a nova atividade.
Nesse ponto, execute o projeto novamente e verifique se a interface com abas continua sendo exibida.
1.8.4. Reescrita dos fragmentos
Vamos revisar o gerenciamento dos fragmentos do projeto. Por enquanto, a classe [PlaceholderFragment] é uma classe interna estática da atividade [MainActivity]. Vamos voltar a um caso de uso mais comum, aquele em que os fragmentos são definidos em classes externas. Além disso, introduziremos as anotações AA para os fragmentos.
O projeto [Exemple-07] evolui da seguinte forma:
![]() |
Acima, vemos surgir a classe [PlaceholderFragment], que foi externalizada para fora da classe [MainActivity]. Ela é reescrita da seguinte forma:
package exemples.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// um fragmento é uma visualização exibida por um contêiner de fragmentos
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// componente da interface visual
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// n.º do fragmento
private static final String ARG_SECTION_NUMBER = "section_number";
// construtor
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("PlaceholderFragment", String.format("afterViews %s", getArguments().getInt(ARG_SECTION_NUMBER)));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("PlaceholderFragment", String.format("onCreateView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onResume() {
Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// pai
super.onResume();
// exibição
if (textViewInfo != null) {
Log.d("PlaceholderFragment", String.format("onResume setText %s", getArguments().getInt(ARG_SECTION_NUMBER)));
textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}
}
}
- linha 15: o fragmento é anotado com a anotação [@EFragment], cujo parâmetro é o identificador da visualização XML associada ao fragmento, neste caso, a visualização [fragment_main.xml];
- linhas 19-20: inserem no campo [textViewInfo] a referência do componente de [fragment_main.xml] identificado por [R.id.section_label], que é um tipo [TextView] (linha (l) abaixo):
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- linhas 42-52: o método [onResume] é executado antes da exibição da visualização associada ao fragmento. Ele pode ser usado para atualizar a interface visual que será exibida;
- linha 47: é necessário chamar o método com o mesmo nome da classe pai;
- linha 49: há uma dúvida sobre se o método [onResume] pode ou não ser executado antes da inicialização do campo da linha 20. Os logs configurados para acompanhar o ciclo de vida do fragmento nos darão essa informação. Por enquanto, e por precaução, fazemos um teste de nulidade;
- linha 51: atualizamos as informações do campo [textViewInfo] com o argumento inteiro passado ao fragmento durante sua criação;
A classe [MainActivity] perde sua classe interna [PlaceholderFragment] e vê seu gerenciador de fragmentos evoluir da seguinte forma:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// os fragmentos
private Fragment[] fragments;
// número de fragmentos
private static final int FRAGMENTS_COUNT = 3;
// n.º do fragmento
private static final String ARG_SECTION_NUMBER = "section_number";
// fabricante
public SectionsPagerAdapter(FragmentManager fm) {
// pai
super(fm);
// inicialização da tabela de fragmentos
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length; 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);
}
}
// fragmento n.º posição
@Override
public Fragment getItem(int position) {
Log.d("MainActivity", String.format("getItem[%s]", position));
return fragments[position];
}
// retorna o número de fragmentos gerenciados
@Override
public int getCount() {
return fragments.length;
}
// opcional — atribui um título aos fragmentos gerenciados
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
}
- linha 4: os fragmentos são colocados em um array;
- linhas 16-23: a inicialização da matriz de fragmentos é feita no construtor. Eles são do tipo [PlaceholderFragment_] (linha 18) e não [PlaceholderFragment]. A classe [PlaceholderFragment] foi, de fato, anotada com uma anotação AA e dará origem a uma classe [PlaceholderFragment_] derivada de [PlaceholderFragment], e é essa classe que a atividade deve utilizar. Cada fragmento criado recebe um argumento inteiro que será exibido pelo fragmento;
- linhas 42-45: alteramos os títulos dos fragmentos. Como esses títulos também são os nomes das abas, devemos observar uma mudança na barra de abas;
Vamos compilar [Make] e [1] neste projeto:
![]() | ![]() |
- em [2], percebe-se que as classes geradas pela biblioteca AA estão na pasta [app / build / generated / source / apt / debug] (é preciso estar na perspectiva [Project] para ver [2]);
Execute o projeto [Exemple-07] e verifique se ele ainda funciona.
1.8.5. Análise dos logs
Ao iniciar o aplicativo, os logs são os seguintes:
- linha 1: construção da única atividade;
- linha 2: método [afterViews] da atividade: seus campos anotados por [@ViewById] são inicializados;
- linhas 3-5: construção dos três fragmentos;
- linhas 6-7: o contêiner de fragmentos [ViewPager] solicita os dois primeiros fragmentos;
- linhas 8-9: métodos do fragmento 2;
- linhas 10-11: métodos do fragmento 1;
- linhas 12-13: método [onResume] do fragmento 1;
- linhas 14-15: método [onResume] do fragmento 2;
- linha 16: criação do menu da atividade;
Observe-se que temos a resposta a uma pergunta feita anteriormente: o método [onResume] do fragmento 1, por exemplo (linha 12), é executado após o método [afterViews] do fragmento (linha 11). Portanto, quando o método [onResume] é executado, ele pode utilizar os campos anotados pelo [@ViewById]. Assim, agora poderemos escrever o método [onResume] da seguinte maneira:
@Override
public void onResume() {
Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// pai
super.onResume();
// exibição
textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}
Agora, vamos passar da aba 1 para a aba 2. Os novos logs são os seguintes:
- linha 1: o contêiner de fragmentos [ViewPager] solicita o fragmento nº 3;
- linhas 2-3: métodos do fragmento nº 3. Vale lembrar que esse fragmento havia sido instanciado logo no início da aplicação;
- linhas 4-5: o método [onResume] do fragmento nº 3 é executado. Vale lembrar que é o fragmento nº 2 que está sendo exibido;
Agora, vamos passar da aba 2 para a aba 3. Não há nenhum registro de log. Portanto, nenhum dos métodos [onCreateView, afterViews, onResume] do fragmento nº 3 é executado. O texto [Hello World from section:3] é exibido corretamente apenas porque esse texto já havia sido criado na etapa anterior, durante a exibição do fragmento nº 2. Lembramos, de fato, que nessa etapa o método [onResume] do fragmento nº 3 havia sido executado. Percebe-se aqui que, assim como o método [onCreateView], o método [onResume] também não pode ser utilizado para atualizar o fragmento 3. Se fosse necessário alterar o texto exibido pelo fragmento, nenhum desses dois métodos seria capaz de fazê-lo.
Agora, voltemos da aba nº 3 para a aba nº 1. Os registros são, então, os seguintes:
Vemos que todos os métodos do fragmento 1 foram executados. Vemos que o método getItem não foi chamado. Como já foi dito, esse método é chamado apenas uma vez para cada fragmento;
Agora, vamos passar da aba 1 para a aba adjacente 2. Temos os seguintes registros:
Surpreendente, não é? Todos os métodos do fragmento nº 3 são reexecutados.
Para entender esses fenômenos, é preciso lembrar que, por padrão, quando o contêiner de fragmentos vai exibir o fragmento i, ele inicializa os fragmentos i-1, i e i+1. Vamos reler os logs à luz dessa informação.
Primeiramente, os logs na inicialização do aplicativo:
Como o contêiner de fragmentos exibirá o fragmento 1, os fragmentos 1 e 2 são inicializados (linhas 8-15).
Agora, passamos da aba 1 para a aba 2:
Como o contêiner de fragmentos exibirá o fragmento 2, os fragmentos 1, 2 e 3 devem ser inicializados. Os fragmentos 1 e 2 já estão inicializados desde a etapa anterior. O fragmento 3 é inicializado nas linhas 2 a 5.
Passamos da aba 2 para a aba 3. Não há registros. Como o contêiner de fragmentos exibirá o fragmento 3, os fragmentos 2 e 3 devem ser inicializados. No entanto, desde a etapa anterior, eles já estão inicializados. O que não vemos aqui é que o fragmento 1, que não é adjacente ao fragmento 3, perde seu estado, que não é mantido na memória.
Passamos da aba 3 para a aba 1. Os logs são os seguintes:
Como o contêiner de fragmentos exibirá o fragmento 1, o fragmento 2 também precisa ser inicializado. Ele já está inicializado desde a etapa anterior. Nessa mesma etapa, o estado do fragmento 1 havia sido perdido. Portanto, ele é reinicializado nas linhas 1 a 4. O que não vemos aqui é que o fragmento 3, que não é adjacente ao fragmento 1, perde seu estado, que, portanto, não é mantido na memória.
Ao passar da aba 1 para a aba adjacente 2, temos os seguintes registros:
Como o contêiner de fragmentos exibirá o fragmento 2, os fragmentos 1, 2 e 3 precisam ser inicializados. Os fragmentos 1 e 2 já estão inicializados desde a etapa anterior. O fragmento 3 é inicializado nas linhas 1 a 4.
O que aprendemos?
- que o gerenciamento padrão dos fragmentos é muito específico e que é preciso conhecê-lo para não enlouquecer. É possível alterar esse modo de gerenciamento, e faremos isso um pouco mais adiante;
- que, com esse gerenciamento padrão, nenhum dos métodos [onCreateView, onResume] pode ser usado para atualizar o fragmento que será exibido, pois não há garantia de que eles serão executados;
1.8.6. onDestroyView
O método [onDestroyView] faz parte do ciclo de vida dos fragmentos (ver parágrafo 1.7.6):
![]() | ![]() |
Observa-se que, no ciclo de vida de um fragmento:
- o método [onCreateView] pode ser executado várias vezes;
- antes de retornar ao método [onCreateView] posteriormente, há necessariamente uma passagem pelo método [onDestroyView] [2];
Vamos inserir esses métodos nos fragmentos para acompanhar melhor seu ciclo de vida. O código do fragmento fica assim:
package exemples.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// um fragmento é uma visualização exibida por um contêiner de fragmentos
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
...
@Override
public void onDestroyView() {
// log
Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// pai
super.onDestroyView();
}
}
Vamos executar o aplicativo. Os primeiros registros são os seguintes:
- linha 1: construção da única atividade;
- linha 2: método [afterViews] da atividade: seus campos anotados por [@ViewById] são inicializados;
- linhas 3-5: construção dos três fragmentos;
- linhas 6-7: o contêiner de fragmentos [ViewPager] solicita os dois primeiros fragmentos;
- linhas 8-9: a visualização do fragmento 2 é criada (não necessariamente tornada visível);
- linhas 10-11: a visualização do fragmento 1 é criada (não necessariamente tornada visível);
- linhas 12-13: método [onResume] do fragmento 1;
- linhas 14-15: método [onResume] do fragmento 2;
- linha 16: criação do menu da atividade;
Vamos passar da aba 1 para a aba 3:
06-03 02:50:02.685 2346-2346/exemples.android D/MainActivity: getItem[2]
06-03 02:50:02.685 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: afterViews 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 02:50:03.024 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 1
- linha 1: o contêiner de fragmentos solicita o terceiro fragmento;
- linhas 2-3: a visualização do fragmento 3 é criada (não necessariamente exibida);
- linhas 4-5: o método [onResume] do fragmento 3 é executado;
- linha 6: o método [onDestroyView] do fragmento 1 é executado. Isso significa que, quando o usuário voltar ao fragmento 1 ou a um fragmento adjacente, o ciclo de vida desse fragmento será executado novamente;
Voltamos da aba 3 para a aba 1:
06-03 02:53:46.255 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:53:46.604 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 3
- linhas 1-4: o ciclo de vida do fragmento 1 é reexecutado porque ele havia passado por um [onDestroyView];
- linha 5: agora é o fragmento 3 que tem seu método [onDestroyView] executado. Novamente, quando o usuário voltar ao fragmento 3 ou a um fragmento adjacente, o ciclo de vida desse fragmento será reexecutado;
1.8.7. setUserVisibleHint
O método [onCreateView] do ciclo de vida instancia a visualização associada ao fragmento, mas não a torna necessariamente visível. É isso que veremos agora. O método [Fragment.setUserVisibleHint] é executado sempre que a visibilidade do fragmento muda. Adicionamos esse método ao código do fragmento:
package exemples.android;
....
// um fragmento é uma visualização exibida por um contêiner de fragmentos
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// componente da interface visual
@ViewById(R.id.section_label)
protected TextView textViewInfo;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
}
}
No início, os logs são os seguintes:
06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
- os logs das linhas 7, 9-10 mostram que apenas o fragmento 1 se torna visível. Também vemos que ele se torna visível antes da execução de seu método [onCreateView];
Vamos passar da aba 1 para a aba 2:
06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
- o fragmento 1 está oculto (linha 3), o fragmento 2 está exibido (linha 4);
Vamos passar da aba 2 para a aba 3:
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
- o fragmento 2 está oculto (linha 1), o fragmento 3 está exibido (linha 2);
Voltemos à aba 1:
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
- o fragmento 3 está oculto (linha 2), o fragmento 1 está exibido (linha 3);
O que aprendemos?
- o método [setUserVisibleHint] é executado uma vez com a propriedade [isVisibleToUser] definida como true, para o fragmento que será exibido;
- não é possível determinar quando esse método será executado em relação ao ciclo de vida do fragmento. Assim, para o fragmento 1, o método [setUserVisibleHint, true] foi executado antes do método [onCreateView], no início do ciclo de vida desse fragmento, enquanto que, para os fragmentos 2 e 3, ocorreu o contrário;
1.8.8. setOffscreenPageLimit
Os logs anteriores mostram que, quando o contêiner de fragmentos [ViewPager] se prepara para exibir o fragmento nº i, ele executa, caso ainda não tenha feito isso, o ciclo de vida dos fragmentos adjacentes i-1 e i+1. Esse funcionamento pode ser controlado pelo método [ViewPager].setOffscreenPageLimit:
Com a instrução acima,
- quando o contêiner de fragmentos [ViewPager] se prepara para exibir o fragmento nº i, ele executa, caso ainda não tenha sido feito, o ciclo de vida dos fragmentos adjacentes do intervalo [i-n, i+n];
- se, em seguida, for exibido o fragmento j:
- o mesmo fenômeno se repete para os fragmentos adjacentes do intervalo [j-n, j+n];
- os fragmentos inicializados na etapa 1 e que não estão mais na vizinhança [j-n, j+n] do novo fragmento podem, então, ser submetidos a uma operação [onDestroyView]. No entanto, pude observar em outras aplicações, notadamente na do capítulo 3, que isso não ocorria sistematicamente;
Modificamos o método [MainActivity.afterViews] da seguinte maneira:
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
// barra de ferramentas
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// o 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.setAdapter(mSectionsPagerAdapter);
// o deslize entre fragmentos é desativado
mViewPager.setSwipeEnabled(false);
// deslocamento dos fragmentos
mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);
// a barra de abas também está associada ao contêiner de fragmentos
// ou seja, a aba nº i exibe o fragmento nº i do contêiner
tabLayout.setupWithViewPager(mViewPager);
// botão flutuante
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
- linha 20: definimos o número de fragmentos adjacentes a serem inicializados como o número total de fragmentos menos 1. Assim, no início, quando o contêiner de fragmentos exibir o fragmento nº 1, ele inicializará simultaneamente os fragmentos 2, 3, ..., n, com n = 1 + mSectionsPagerAdapter.getCount() - 1 = mSectionsPagerAdapter.getCount(). Portanto, todos esses fragmentos serão inicializados. Quando a janela de exibição se deslocar para outro fragmento, o contêiner de fragmentos:
- perceberá que todos os fragmentos adjacentes ao novo fragmento já estão inicializados e, portanto, não os inicializará;
- como a adjacência do novo fragmento também abrange a totalidade dos fragmentos, nenhum será “desinicializado” pelo contêiner de fragmentos;
No total, deveríamos ver todos os fragmentos instanciados e inicializados na inicialização do aplicativo e, depois disso, nunca mais. É isso que vamos verificar agora, examinando os logs.
Ao iniciar, temos os seguintes logs:
- linhas 4-6: construção dos três fragmentos;
- linhas 7, 9, 11: o contêiner de fragmentos solicita os três fragmentos. Na versão anterior, ele solicitava dois;
- linhas 14-25: o ciclo de vida dos três fragmentos é executado;
Vamos agora passar da aba 1 para a aba 2:
Vamos passar da aba 2 para a aba 3:
Em seguida, da aba 3 para a aba 1:
Os logs confirmam a teoria. Todos os fragmentos foram instanciados e inicializados na inicialização. Depois disso, os métodos de seu ciclo de vida não são mais executados. Temos aqui um funcionamento muito previsível dos fragmentos, o que facilita enormemente seu uso.
O que queremos descobrir é uma maneira de atualizar um fragmento que será exibido, independentemente da adjacência de fragmentos escolhida pelo desenvolvedor. Os logs nos mostraram duas coisas:
- o método [setUserVisibleHint, true] é sempre executado para o fragmento que será exibido e não para os demais;
- esse evento pode ocorrer antes ou depois do ciclo de vida do fragmento. Isso depende da adjacência de fragmentos escolhida pelo desenvolvedor. Isso é um problema, pois, se o ciclo de vida ainda não tiver ocorrido, isso significa que o fragmento não pode ser atualizado pelo método [setUserVisibleHint, true];
Os logs na inicialização do aplicativo, quando a adjacência dos fragmentos era 1, foram os seguintes:
06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
- percebe-se que, quando o fragmento 1 se torna visível, sua visualização ainda não foi criada. Portanto, não é possível alterá-la. Isso poderá ser feito durante o ciclo de vida do fragmento, por exemplo, nos métodos [onCreateView] (linha 11) ou [onResume] (linhas 13-14). Como utilizamos as anotações AA, normalmente não precisamos escrever o método [onCreateView]. Portanto, o método [onResume] parece ser o mais adequado aqui para atualizar o fragmento 1;
Quando passamos da guia 1 para a guia 2, os logs foram os seguintes:
06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
Desta vez, temos apenas o método [setUserVisibleHint, true] da linha 4 para atualizar o fragmento 2;
Quando passamos da aba 2 para a aba 3, os logs foram os seguintes:
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
Aqui, temos apenas o método [setUserVisibleHint, true] da linha 2 para atualizar o fragmento 3;
Quando passamos da aba 3 para a aba 1, os logs foram os seguintes:
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
Aqui, é preciso usar o método [onResume] do fragmento 1 (linhas 6-7) para atualizar o fragmento 1.
Portanto, neste exemplo, vemos que, para atualizar um fragmento que será exibido, temos dois métodos à disposição: [setUserVisibleHint] e [onResume].
Vamos implementar essa solução em um novo projeto, no qual cada fragmento deverá exibir o número de vezes em que foi exibido, o que chamaremos de visita. Portanto, será necessário atualizar sua exibição sempre que ele for exibido. Esse é exatamente o problema que estamos tentando resolver.
Antes disso, vamos examinar a última etapa do ciclo de vida de uma atividade ou de um fragmento: aquela em que ele é destruído. O sistema pode tomar a iniciativa de excluir uma atividade se outras atividades com maior prioridade solicitarem recursos indisponíveis. Para liberar esses recursos, o sistema tomará a iniciativa de excluir certas atividades. O método [onDestroy] da atividade e dos fragmentos será então chamado.
1.8.9. OnDestroy
![]() | ![]() | ![]() |
Permitiremos que o usuário exclua a atividade por meio de uma opção de menu [5]. Para isso, adicionamos uma nova opção de menu no arquivo [menu_main.xml] [1]:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/action_terminate"
android:title="@string/action_terminate"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
Basta copiar e colar a primeira opção do menu e adaptar o resultado (linhas 9 e 10). O texto dessa nova opção é adicionado ao arquivo [strings.xml] [2]:
<resources>
<string name="app_name">Exemple-07</string>
<string name="action_settings">Settings</string>
<string name="action_terminate">Terminate</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
Por fim, na classe [MainActivity], gerenciamos o clique na opção [Terminate]:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// Trate aqui os cliques nos itens da barra de ação. A barra de ação irá
// tratar automaticamente os cliques no botão Início/Acima, desde que
// você especificar uma atividade pai em AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
if (id == R.id.action_terminate) {
Log.d("menu", "action_terminate selected");
//ao encerrar a atividade
finish();
return true;
}
// pai
return super.onOptionsItemSelected(item);
}
- linhas 14-19: faz-se um copiar/colar das linhas 10-13 e adapta-se o código à nova opção;
- linha 17: a atividade é encerrada por ação do software;
Agora, vamos executar essa nova versão e, assim que a primeira visualização for exibida, clicar na opção de menu [Terminate]. Os logs ficam assim:
- linhas 1-2: clique na opção [Terminate];
- linha 4: o método [onDestroy] da atividade é chamado;
- linhas 4-5: o método [onDestroyView] do fragmento 1 é chamado, seguido pelo método [onDestroy];
- linhas 6-9: essa operação se repete para os outros dois fragmentos;
Lembre-se, portanto, de que o método [onDestroy] da atividade e dos fragmentos é chamado quando a atividade está prestes a ser excluída pelo sistema, pelo desenvolvedor ou pelo usuário. É possível usar esse método para salvar informações, por exemplo, localmente no tablet, a fim de recuperá-las quando o usuário reiniciar o aplicativo.
1.9. Exemplo-08: atualização de um fragmento com adjacência variável de fragmentos
1.9.1. Criação do projeto
Duplica-se o projeto [Exemple-07] em [Exemple-08]. Para isso, segue-se o procedimento descrito para duplicar [Exemple-02] em [Exemple-03] no parágrafo 1.4.
![]() | ![]() |
1.9.2. Reescrita do fragmento [PlaceholderFragment]
O novo código do fragmento [PlaceholderFragment] é o seguinte. Ele funciona independentemente da adjacência atribuída aos fragmentos (1, parcial, total):
package exemples.android;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// um fragmento é uma visualização exibida por um contêiner de fragmentos
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// componente da interface visual
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// dados
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 0;
// nº do fragmento
private static final String ARG_SECTION_NUMBER = "section_number";
// construtor
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// log
Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
if (!initDone) {
// texto inicial
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// inicialização concluída
initDone = true;
}
// exibição do texto atual
textViewInfo.setText(text);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
@Override
public void onDestroyView() {
...
}
@Override
public void onResume() {
...
}
// atualização de fragmento
public void update() {
// o trabalho a ser feito depende do número da visita
if (numVisit > 1) {
// registro
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// texto alterado
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// informações locais para registros
private String getInfos() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s", numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone);
}
}
- linhas 34-48: o método [@AfterViews] pode ser executado várias vezes. Ele era usado para inicializar o texto do fragmento (linha 42). Continuamos fazendo isso, mas, para que ocorra apenas uma vez, gerenciamos uma variável booleana [initDone] (linha 44) para indicar que a inicialização já foi feita e não precisa ser repetida;
- linhas 56-59: introduzimos o método [onDestroyView] para registrar que, na próxima vez que o fragmento for exibido novamente, seu ciclo de vida será reexecutado;
- os logs mostraram que dois métodos podem ser executados após o método [@AfterViews]: os métodos [setUserVisibleHint] e [onResume]. O método [onResume] só é executado quando o ciclo de vida do fragmento é executado. Já o método [setUserVisibleHint] nem sempre é executado após o método [@AfterViews]. Os logs mostraram que pelo menos um dos dois é executado após o método [@AfterViews]. Os logs nunca mostraram que ambos pudessem ser executados juntos após o método [@AfterViews]. É um ou outro. Por precaução, definiremos um booleano [updateDone] quando uma atualização for realizada;
Os métodos [setUserVisibleHint] e [onResume] são os seguintes:
// dados
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 0;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// pai
super.setUserVisibleHint(isVisibleToUser);
// memória
this.isVisibleToUser = isVisibleToUser;
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// número de visitas
if (isVisibleToUser) {
// incremento
numVisit++;
// atualização do fragmento
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// o fragmento será armazenado em cache
updateDone = false;
}
}
@Override
public void onResume() {
// pai
super.onResume();
// registro
Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// atualização
if (isVisibleToUser && !updateDone) {
update();
updateDone = true;
}
}
- linha 14: armazena-se o estado de visibilidade do fragmento;
- linhas 22-25: se o fragmento estiver visível e o método [@AfterViews] tiver sido executado, o método [update] é executado e a variável booleana [updateDone] é passada para true;
- linhas 26-28: se o fragmento for ocultado, a variável booleana [updateDone] é redefinida para false. De fato, precisamos de um evento para reinicializar a variável booleana [updateDone] — que foi definida como true — para false assim que o método [update] for chamado, para que novas atualizações possam ser realizadas. Aproveitamos o fato de que o fragmento não está mais visível para fazer isso. Quando ele voltar a ficar visível, a atualização do fragmento deverá ser realizada novamente;
- linhas 32-42: os logs mostram que, dependendo da adjacência escolhida para os fragmentos, o método [onResume] pode ser executado mesmo quando o fragmento não está visível. Se ele não estiver visível, não fazemos a atualização (linha 39) e, assim como fizemos para [setMenuVisibility], gerenciamos o booleano [updateDone].
Por fim, o método [onDestroyView] é o seguinte:
@Override
public void onDestroyView() {
// pai
super.onDestroyView();
// atualização do indicador
afterViewsDone = false;
// registro
Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}
O método [onDestroyView] é executado quando um ciclo de vida do fragmento chega ao fim. Outro ciclo poderá ser reiniciado posteriormente.
- linha 6: o método [onDestroyView] remove qualquer ligação com a visualização associada ao fragmento. Ela será recriada no próximo ciclo de vida do fragmento. Por enquanto, precisamos definir o booleano [afterViews] como false, para indicar que a ligação com a visualização não existe mais;
Vamos executar o aplicativo com 5 fragmentos com adjacência igual a 2. As alterações são feitas em [MainActivity]:
// número de fragmentos
private final int FRAGMENTS_COUNT = 5;
// adjacência dos fragmentos
private final int OFF_SCREEN_PAGE_LIMIT=2;
// gerenciador de fragmentos
private SectionsPagerAdapter mSectionsPagerAdapter;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
....
// deslocamento dos fragmentos
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
...
}
Os logs na inicialização são os seguintes:
05-31 06:23:07.015 32551-32551/exemples.android D/MainActivity: constructor
05-31 06:23:07.041 32551-32551/exemples.android D/MainActivity: afterViews
05-31 06:23:07.050 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[0]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[1]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[2]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.280 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.291 32551-32551/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.294 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.798 32551-32551/exemples.android D/menu: création menu en cours
- linhas 8, 10, 12: o contêiner de fragmentos solicita todos os fragmentos adjacentes ao fragmento 1;
- linhas 9, 11, 13: o método [setUserVisibleHint] desses fragmentos é executado com [visibleToUser] até false;
- linha 14: o método [setUserVisibleHint] do fragmento 1 é executado com [visibleToUser] até true;
- linhas 15-17: o método [afterViews] dos três segmentos adjacentes é chamado. Vemos, portanto, aqui um caso em que esse método é chamado depois que um fragmento se torna visível (o fragmento 1, linha 14);
- linhas 18-20: o método [onResume] dos três segmentos adjacentes é chamado;
Passamos da aba 1 para a aba 2:
05-31 06:52:36.132 32551-32551/exemples.android D/MainActivity: getItem[3]
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
- como a adjacência dos fragmentos é deslocada uma posição para a direita, o fragmento 4 é solicitado pelo contêiner de fragmentos;
- linha 2: o método [setUserVisibleHint] do fragmento 4 é chamado com [visibleToUser] até false;
- linha 3: o método [setUserVisibleHint] do fragmento 1 é chamado com [visibleToUser] até false. De fato, o fragmento 1 agora está oculto;
- linha 4: o método [setUserVisibleHint] do fragmento 2 é chamado com [visibleToUser] até true. O fragmento 2 agora está visível;
- linhas 5-6: o ciclo de vida do fragmento 4 continua;
Passamos da aba 2 para a aba 3:
05-31 06:58:16.228 32551-32551/exemples.android D/MainActivity: getItem[4]
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
- como a adjacência dos fragmentos é deslocada uma posição para a direita, o fragmento 5 é solicitado pelo contêiner de fragmentos;
- linha 2: o método [setUserVisibleHint] do fragmento 5 é chamado com [visibleToUser] até false;
- linha 3: o método [setUserVisibleHint] do fragmento 2 é chamado com [visibleToUser] até false. De fato, o fragmento 2 agora está oculto;
- linha 4: o método [setUserVisibleHint] do fragmento 3 é chamado com [visibleToUser] em true. O fragmento 3 agora está visível;
- linhas 5-6: o ciclo de vida do fragmento 5 continua;
Passamos da aba 3 para a aba 4:
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- linha 1: o fragmento 3 agora está oculto;
- linha 2: o fragmento 4 agora está visível. Observe-se que não há execução do ciclo de vida do fragmento 4. Isso já foi feito duas etapas antes;
- linha 3: o fragmento 1 sai da adjacência do fragmento 4 exibido. Seu método [onDestroyView] é executado. Na próxima vez que for exibido, seu ciclo de visualização [onCreateView, afterViews, onResume] será executado novamente;
Passamos da aba 4 para a aba 5:
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- linha 1: o fragmento 4 agora está oculto;
- linha 2: o fragmento 5 agora está visível. Observe-se que não há execução do ciclo de vida do fragmento 5. Isso já foi feito duas etapas antes;
- linha 3: o fragmento 2 sai da adjacência do fragmento 5 exibido. Seu método [onDestroyView] é executado;
Passamos da aba 5 para a aba 1:
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 4 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 5 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- linhas 1, 4, 5, 6: o ciclo de vida do fragmento 1 é reexecutado. Na verdade, ele havia perdido a conexão com sua visualização;
- linhas 2, 5, 8, 9: pelo mesmo motivo, o ciclo de vida do fragmento 2 é reexecutado;
- linhas 10-11: os fragmentos 4 e 5 saem da adjacência do fragmento exibido;
- linha 7: o fragmento 1 é atualizado;
![]() |
Os logs nunca mostraram que os métodos [setUserVisibleHint] e [onResume] tentassem, ambos, atualizar o fragmento. É um ou outro. Recomenda-se ao leitor que realize outros testes e acompanhe os logs para compreender bem os conceitos de adjacência e ciclo de vida dos fragmentos.
Agora, vamos supor uma adjacência total e realizar os mesmos testes.
No [MainActivity]:
// número de fragmentos
private final int FRAGMENTS_COUNT = 5;
// adjacência dos fragmentos
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;
Os logs na inicialização são os seguintes:
05-31 07:34:44.717 28908-28908/exemples.android D/MainActivity: constructor
05-31 07:34:44.844 28908-28908/exemples.android D/MainActivity: afterViews
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[0]
05-31 07:34:45.201 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[1]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[2]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[3]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/MainActivity: getItem[4]
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.207 28908-28908/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.209 28908-28908/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:46.548 28908-28908/exemples.android D/menu: création menu en cours
- os logs mostram que o ciclo de vida dos 5 fragmentos é executado;
- o fragmento 1 é exibido na linha 18;
Passamos da aba 1 para a aba 2:
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- linha 1: o fragmento 1 é ocultado;
- linha 2: o fragmento 2 é exibido;
Passamos da aba 2 para a aba 3:
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- linha 1: o fragmento 2 está oculto;
- linha 2: o fragmento 3 é exibido;
Passamos da aba 3 para a aba 4:
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- linha 1: o fragmento 3 está oculto;
- linha 2: o fragmento 4 é exibido;
Passamos da aba 4 para a aba 5:
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- linha 1: o fragmento 4 está oculto;
- linha 2: o fragmento 5 é exibido;
Passamos da aba 5 para a aba 1:
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- linha 1: o fragmento 5 está oculto;
- linha 2: o fragmento 1 é exibido;
- linha 3: o fragmento 1 é atualizado;
Passamos da aba 1 para a aba 4:
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: update 4 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- linha 1: o fragmento 1 está oculto;
- linha 2: o fragmento 4 é exibido;
- linha 3: o fragmento 4 é atualizado;
Percebe-se que, com a adjacência total, o comportamento dos fragmentos é muito mais previsível.
Agora, vamos definir a adjacência como nula e ver o que acontece. A classe [MainActivity] evolui da seguinte forma:
// número de fragmentos
private final int FRAGMENTS_COUNT = 5;
// adjacência dos fragmentos
private final int OFF_SCREEN_PAGE_LIMIT = 0;
Os logs na inicialização são os seguintes:
06-01 03:11:52.068 5679-5679/exemples.android D/MainActivity: constructor
06-01 03:11:52.353 5679-5679/exemples.android D/MainActivity: afterViews
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[0]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[1]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.571 5679-5679/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
06-01 03:11:54.597 5679-5679/exemples.android D/menu: création menu en cours
- nas linhas 8 e 10, vemos que o contêiner de fragmentos solicitou 2 fragmentos, os n.ºs 1 e 2. Tudo ocorre, portanto, como se houvesse uma adjacência de 1. A adjacência de 0 foi, portanto, ignorada.
1.9.3. Comunicação entre fragmentos
Na arquitetura anterior, temos uma atividade e n fragmentos. O usuário interage com os diferentes fragmentos. Essas interações alteram o estado do aplicativo. Chamamos aqui de “estado do aplicativo” o conjunto de informações que ele armazena ao longo de sua vida útil. Surge então o seguinte problema:
- quando o usuário interage com o fragmento i, o aplicativo passa de um estado E1 para um estado E2;
- uma ação do usuário no fragmento i faz com que o fragmento j seja exibido;
- como atualizar o fragmento j com o estado atual E2 do aplicativo;
Pelos exemplos anteriores, sabemos como atualizar o fragmento j. Mas onde encontrar o estado E2 do aplicativo para atualizá-lo?
Existem diferentes soluções para esse problema. Vimos uma delas: o fragmento i pode transmitir o estado E2 do aplicativo ao fragmento j por meio de argumentos. Encontramos esse método na classe [MainActivity] durante a criação dos fragmentos:
for (int i = 0; i < fragments.length; 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);
}
Essa solução não é imediatamente aplicável aqui. De fato, quando o usuário clica na aba j, o que faz com que o fragmento j seja exibido, nosso código não é chamado. Apenas o código do sistema é executado. Veremos em um próximo projeto como interceptar o clique em uma aba, mas, por enquanto, vamos adotar outra abordagem.
Já falamos sobre o estado da aplicação: o conjunto de dados gerenciados pela aplicação ao longo do tempo. Aqui, a aplicação é composta por uma atividade e n fragmentos, todos instanciados uma única vez ao iniciar a aplicação e cuja vida útil é igual à da aplicação. Portanto, cada um desses elementos, ou vários deles em conjunto, podem ser candidatos a armazenar o estado da aplicação. Cada fragmento tem acesso, por meio do método [Fragment.getActivity()], à atividade que o criou. Como todos os fragmentos têm acesso à atividade, parece natural armazenar o estado da aplicação nela.
No entanto, o resultado do método [Fragment.getActivity()] depende do momento em que ele é chamado no ciclo de vida. Ilustramos esse ponto adicionando alguns registros de log na classe [PlaceholderFragment]:
// atualizar fragmento
public void update() {
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// o trabalho a ser realizado depende do número da visita
if (numVisit > 1) {
// registro
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// texto modificado
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// informações locais para registros
private String getInfos() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s, getActivity()==null:%s",
numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone, getActivity() == null);
}
- linhas 14-16: o método [getInfos] exibe parte do relatório do aplicativo;
Iniciamos a aplicação com uma adjacência de fragmentos igual a 2. Os logs na inicialização da aplicação:
06-01 03:26:13.769 10931-10931/exemples.android D/MainActivity: constructor
06-01 03:26:13.856 10931-10931/exemples.android D/MainActivity: afterViews
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:14.535 10931-10931/exemples.android D/MainActivity: getItem[0]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[1]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[2]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.541 10931-10931/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.545 10931-10931/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:15.967 10931-10931/exemples.android D/menu: création menu en cours
- linhas 9, 10, 13, 14: vemos que, nos métodos [setUserVisibleHint], temos [getActivity()==null] se o fragmento ainda não estiver visível (isVisibleToUser == false);
- linha 19: percebe-se que, quando o fluxo de execução chega ao método [update] do fragmento 1, o método [getActivity] retorna corretamente a atividade;
Quando se define a adjacência dos fragmentos como 4 (adjacência total), os logs são os seguintes:
06-01 03:35:23.553 2814-2814/exemples.android D/MainActivity: constructor
06-01 03:35:23.751 2814-2819/exemples.android I/art: Ignoring second debugger -- aceitação e rejeição
06-01 03:35:23.900 2814-2814/exemples.android D/MainActivity: afterViews
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.002 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[0]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[1]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[2]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[3]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[4]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.210 2814-2814/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.211 2814-2814/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.214 2814-2814/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:26.602 2814-2814/exemples.android D/menu: création menu en cours
Os resultados são os mesmos. Conclui-se que, assim que o fragmento fica visível, o método [getActivity] retorna a atividade do fragmento. Observa-se também que, quando a execução chega ao método [update] do fragmento que será exibido, o método [getActivity] retorna corretamente um valor.
Para ilustrar a comunicação entre fragmentos, criaremos um novo projeto.
1.10. Exemplo-09: comunicação entre fragmentos, deslizar e rolagem
1.10.1. Criação do projeto
Duplicamos o projeto [Exemple-07] em [Exemple-08]. Para isso, seguiremos o procedimento descrito para duplicar [Exemple-02] em [Exemple-03] no parágrafo 1.4.
![]() | ![]() |
1.10.2. A sessão
Neste novo projeto, queremos que os fragmentos exibam o número total de fragmentos exibidos pelo usuário. Para isso, é necessário manter um contador que seja acessível a todos os fragmentos. Chamaremos de “sessão” o objeto que encapsula os dados compartilhados pelos fragmentos. Essa terminologia vem do desenvolvimento web, onde se coloca em uma sessão os dados a serem compartilhados por diferentes visualizações solicitadas por um mesmo usuário. O fato de encapsular as informações compartilhadas pelos diferentes fragmentos em um mesmo objeto torna o código mais legível.
A classe [Session] será a seguinte:
![]() |
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// número de fragmentos visitados
private int numVisit;
// getters e setters
public int getNumVisit() {
return numVisit;
}
public void setNumVisit(int numVisit) {
this.numVisit = numVisit;
}
}
- linha 8: a sessão encapsulará o número de fragmentos visitados;
- linha 5: a anotação [EBean] é uma anotação AA. O atributo [scope] designa o escopo (ou tempo de vida) da classe assim anotada. Aqui, o atributo [scope = EBean.Scope.Singleton] faz com que a classe [Session] seja um singleton: ela será instanciada uma única vez, no início da aplicação. A referência a uma classe anotada com [EBean] pode, em seguida, ser injetada em outra classe. Esse é o conceito de injeção de dependências;
1.10.3. A atividade [MainActivity]
A atividade [MainActivity] evolui da seguinte maneira:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
...
// injeção de sessão
@Bean(Session.class)
protected Session session;
// número de fragmentos
private final int FRAGMENTS_COUNT = 5;
// adjacência dos fragmentos
private final int OFF_SCREEN_PAGE_LIMIT = 2;
@AfterInject
protected void afterInject(){
Log.d("MainActivity", "afterInject");
// inicialização da sessão
session.setNumVisit(0);
}
...
- linhas 7-8: injeção da referência ao singleton da sessão por meio da anotação [@Bean]. O parâmetro da anotação é a classe do bean a ser injetado. O campo assim anotado não pode ter o escopo [private];
- linha 15: a anotação [@AfterInject] serve para designar um método a ser chamado quando todas as injeções da classe tiverem sido realizadas. Assim, ao entrar no método [afterInject] da linha 16, a referência da linha 8 já foi inicializada;
- linha 20: o contador de visitas é zerado;
1.10.4. O fragmento [PlaceholderFragment]
O fragmento [PlaceholderFragment] evolui da seguinte forma:
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
....
// sessão
protected Session session;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// pai
super.setUserVisibleHint(isVisibleToUser);
// memória
this.isVisibleToUser = isVisibleToUser;
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// número de visitas
if (isVisibleToUser) {
// atualização do fragmento
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// o fragmento será armazenado em cache
updateDone = false;
}
}
// atualização do fragmento
public void update() {
// registro
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// sessão
if (session == null) {
session = ((MainActivity) getActivity()).getSession();
}
// incrementar número de visitas
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// texto alterado
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- linha 7: a sessão;
- linhas 35-37: sabemos que, ao entrar no método [update], o método [getActivity] retorna corretamente a atividade. Aproveitamos para recuperar a sessão e armazená-la localmente (linha 36);
- linhas 39-41: para incrementar o número da visita, buscamos esse valor na sessão. Poderíamos ter colocado esse código no método [setUserVisibleHint] a partir da linha 19, pois sabemos que, nesse momento, o método [getActivity] retorna a atividade. Decidimos aqui não atribuir nenhuma função específica a esse método e migrar o código específico de um fragmento para o método [update], que foi criado exatamente para isso;
- linha 43: exibe o número da visita;
Ao executar esta aplicação com 5 fragmentos, com uma adjacência de 2 fragmentos, os primeiros registros são os seguintes:
05-31 08:38:47.305 20114-20114/exemples.android D/MainActivity: constructor
05-31 08:38:47.307 20114-20114/exemples.android D/MainActivity: afterInject
05-31 08:38:47.351 20114-20114/exemples.android D/MainActivity: afterViews
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
...
- linhas 2-3: observa-se que o método [afterInject] da atividade é executado antes do método [afterViews];
O leitor é convidado a testar este novo aplicativo.
1.10.5. Desativar o Swipe ou Deslize
No aplicativo anterior, quando se desliza o mouse para a esquerda ou para a direita no emulador Android, a visualização atual dá lugar à visualização da direita ou da esquerda, conforme o caso. Esse comportamento padrão nem sempre é desejável. Vamos aprender a desativar o deslize entre visualizações (swipe).
Voltemos à tela principal XML:
![]() |
No código XML da visualização, encontramos o código do contêiner de fragmentos:
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
A linha 1 indica a classe que gerencia as páginas da atividade. Essa classe pode ser encontrada na atividade [MainActivity]:
import android.support.v4.view.ViewPager;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// o gerenciador de fragmentos
private SectionsPagerAdapter mSectionsPagerAdapter;
// o contêiner de fragmentos
@ViewById(R.id.container)
protected ViewPager mViewPager;
...
Na linha 12, o contêiner de fragmentos é do tipo [android.support.v4.view.ViewPager] (linha 1). Para desativar a varredura, é necessário derivar essa classe da seguinte maneira:
![]() |
package exemples.android;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// controla o deslize
private boolean isSwipeEnabled;
// 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 autorizado?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// setter
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
}
- linha 8: a classe [MyPager] estende a classe Android [ViewPager] (linha 4);
- ao deslizar a mão, os manipuladores de eventos das linhas 24 e 34 podem ser chamados. Ambos retornam um valor booleano. Basta que retornem o valor booleano [false] para inibir o deslizamento;
- linha 11: o valor booleano que serve para indicar se o deslize da mão é aceito ou não.
Feito isso, é preciso agora utilizar nosso novo gerenciador de páginas. Isso é feito na visualização XML [activity_main.xml] e na atividade principal [MainActivity]. Em [activity_main.xml], escreve-se:
![]() |
<exemples.android.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
Na linha 1, utiliza-se a nova classe. Em [MainActivity], o código é alterado da seguinte forma:
package exemples.android;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// o gerenciador de fragmentos
private SectionsPagerAdapter mSectionsPagerAdapter;
// o contêiner de fragmentos
@ViewById(R.id.container)
protected MyPager mViewPager;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
// 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.setAdapter(mSectionsPagerAdapter);
// desativa-se o deslize entre fragmentos
mViewPager.setSwipeEnabled(false);
// a barra de abas também está associada ao contêiner de fragmentos
...
- linha 12: o gerenciador de páginas agora tem o tipo [MyPager];
- linha 23: é possível ativar ou desativar o deslocamento com a mão.
Teste esta nova versão. Desative ou não a rolagem e observe a diferença no comportamento das visualizações ao arrastá-las para a direita ou para a esquerda com o mouse. Em todos os aplicativos futuros, a rolagem estará desativada. Não voltaremos a mencionar isso.
1.10.6. Desativar a rolagem entre fragmentos
Vamos continuar com uma melhoria no gerenciador de abas. Ao passar da aba 1 para a aba 4, vemos as duas abas intermediárias, 2 e 3, rolarem pela tela. Isso é chamado, no jargão do Android, de smoothScrolling. Esse comportamento pode se tornar incômodo se houver muitos abas. Ele pode ser desativado adicionando o seguinte código no gerenciador de fragmentos [MyPager]:
// controla o deslize
private boolean isSwipeEnabled;
// controla a rolagem
private boolean isScrollingEnabled;
...
// rolagem
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// setter
...
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
Como o gerenciador de abas foi associado ao gerenciador de fragmentos [MyPager], ao clicar na aba nº i, o fragmento nº i é exibido pelo contêiner de fragmentos por meio do método [setCurrentItem] acima (linha 9). [position] é o número do fragmento a ser exibido;
- linha 10: é chamado o método [setCurrentItem] da classe pai. O segundo argumento do método [false] solicita que haja uma transição imediata entre o fragmento antigo e o novo (sem rolagem); no método [true], solicita-se que haja uma transição por meio do método scrolling. Aqui, o segundo argumento é o valor do campo da linha 4, campo que o desenvolvedor pode definir com o método das linhas 16 a 18;
Se quisermos desativar a rolagem, a classe [MainActivity] será a seguinte:
...
// deslocamento dos fragmentos
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// inibe o deslize entre fragmentos
mViewPager.setSwipeEnabled(false);
// sem rolagem
mViewPager.setScrollingEnabled(false);
...
Execute o projeto novamente e verifique se não há mais scrolling entre as abas 1 e 4, por exemplo. Daqui em diante, sempre desativaremos a rolagem. Não voltaremos a abordar esse assunto.
1.10.7. Um novo fragmento
No nosso exemplo, todos os fragmentos são do mesmo tipo [PlaceHolderFragment]. Agora vamos aprender a criar um novo fragmento e a exibi-lo.
Primeiro, vamos copiar a visualização [vue1.xml] do projeto [Exemple-04] para o projeto [Exemple-09] [1]:
![]() | ![]() |
- no [1], a visualização [vue1.xml];
- no [3], a visualização apresenta erros decorrentes de textos ausentes no arquivo [res/values/strings.xml];
No [2], adiciona-se os textos ausentes, obtendo-os do arquivo [res/values/strings.xml] do projeto [Exemple-04]
<resources>
<string name="app_name">Exemple-07</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- visualização 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
</resources>
- acima, adicionamos as linhas 6 a 9;
Agora, criamos a classe [Vue1Fragment], que será o fragmento responsável por exibir a visualização [vue1.xml]:
![]() |
A classe [Vue1Fragment] será a seguinte:
package exemples.android;
import android.support.v4.app.Fragment;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends Fragment {
// 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(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- linha 10: a anotação [@EFragment] faz com que o fragmento utilizado pela atividade seja, na verdade, a classe [Vue1Fragment_]. É importante lembrar disso. O fragmento está associado à visualização [vue1.xml];
- linhas 14-15: o componente identificado por [R.id.editTextNom] é injetado no campo [editTextNom] da linha 15;
- linhas 18-20: o método [doValider] gerencia o evento 'click' no botão identificado por [R.id.buttonValider];
- linha 21: o primeiro parâmetro de [Toast.makeText] é do tipo [Activity]. O método [Fragment.getActivity()] permite obter a atividade na qual o fragmento está localizado. Trata-se de [MainActivity], uma vez que, nessa arquitetura, temos apenas uma atividade que exibe diferentes visualizações ou fragmentos;
Na classe [MainActivity], o gerenciador de fragmentos evolui da seguinte forma:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// os fragmentos
private Fragment[] fragments;
// nº do fragmento
private static final String ARG_SECTION_NUMBER = "section_number";
// construtor
public SectionsPagerAdapter(FragmentManager fm) {
// pai
super(fm);
// inicialização da tabela de fragmentos
fragments = new Fragment[FRAGMENTS_COUNT];
for (int 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[fragments.length - 1] = new Vue1Fragment_();
}
...
}
- linha 13: há [FRAGMENTS_COUNT] fragmentos: [FRAGMENTS_COUNT-1] fragmentos do tipo [PlaceholderFragment] (linhas 14-21) e um fragmento do tipo [Vue1Fragment_], linha 23 (atenção ao sublinhado);
Compile e execute o projeto [Exemple-09]. A guia nº 5 deve estar diferente:
![]() |
1.10.8. Derivar todos os fragmentos de uma mesma classe abstrata
O novo fragmento [Vue1Fragment] também precisa ser atualizado quando exibido. Para isso, precisaremos criar um código semelhante ao criado para o fragmento [PlaceholderFragment]. Para evitar repetições, vamos fatorar o que for possível em uma classe abstrata da qual todos os fragmentos do aplicativo herdarão.
Para isso, criamos um novo projeto.
1.11. Exemplo-10: fazer com que todos os fragmentos derivem de uma classe abstrata
1.11.1. Criação do projeto
Duplicamos o projeto [Exemple-09] em [Exemple-10]:
![]() | ![]() |
1.11.2. Gerenciamento do modo de depuração
Adicionamos ao projeto a opção de exibir ou não os logs do modo de depuração. Para isso, adicionamos uma constante estática à classe [MainActivity]:
// modo de depuração
public static final boolean IS_DEBUG_ENABLED = false;
1.11.3. A classe abstrata pai de todos os fragmentos
![]() |
A classe [AbstractFragment] é a seguinte:
package exemples.android;
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
public abstract class AbstractFragment extends Fragment {
// dados privados
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private String className;
// dados acessíveis às classes filhas
protected boolean afterViewsDone = false;
protected boolean isDebugEnabled = true;
// atividade
protected MainActivity activity;
// sessão
protected Session session;
// construtor
public AbstractFragment() {
// inicialização
isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
className = getClass().getSimpleName();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// pai
super.setUserVisibleHint(isVisibleToUser);
...
}
@Override
public void onDestroyView() {
// pai
super.onDestroyView();
...
}
@Override
public void onResume() {
// pai
super.onResume();
...
}
// informações locais
protected String getParentInfos() {
return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
}
// atualização de fragmento
protected void update() {
...
// solicita-se que a classe filha seja atualizada
updateFragment();
}
protected abstract void updateFragment();
}
- linha 7: a classe [AbstractFragment] estende a classe Android [Fragment];
- todo fragmento deve poder ser atualizado. É por isso que a classe pai [AbstractFragment] exige que suas classes filhas possuam um método [updateFragment] (linha 68), que ela chama (linha 65);
- linha 19: a classe armazenará uma referência à atividade do aplicativo;
- linha 22: a classe armazenará uma referência à sessão onde estão reunidos os dados compartilhados pelos fragmentos e pela atividade;
- linhas 25-33: o construtor da classe abstrata;
- linha 27: criação de uma cópia da constante [MainActivity.IS_DEBUG_ENABLED] no campo da linha 16;
- linha 28: armazena-se o nome da classe instanciada, ou seja, o nome de uma classe filha;
- linhas 15-22: esses campos possuem o atributo [protected] para que as classes filhas tenham acesso a eles. Observe-se que as classes filhas ignoram a existência dos booleanos [isVisibleToUser] e [updateDone] (linhas 10-11);
- linha 57: o método [getParentInfos] possui o atributo [protected] para que as classes filhas possam chamá-lo;
Os métodos [setUserVisibleHint, onDestroyView, onResume] permanecem iguais aos que existiam na classe [PlaceholderFragment] do projeto anterior:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// pai
super.setUserVisibleHint(isVisibleToUser);
// memória
this.isVisibleToUser = isVisibleToUser;
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
}
// caso em que o fragmento se tornará visível
if (isVisibleToUser) {
// atualizar fragmento
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// saída do fragmento
updateDone = false;
}
}
@Override
public void onDestroyView() {
// pai
super.onDestroyView();
// atualização do indicador
afterViewsDone = false;
// registro
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
}
}
@Override
public void onResume() {
// pai
super.onResume();
// registro
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
}
if (isVisibleToUser) {
// atualização
if (!updateDone) {
update();
updateDone = true;
}
}
}
O método [update] é o seguinte:
// atualização de fragmento
protected void update() {
// recuperamos a atividade e a sessão
if (activity == null) {
Activity activity = getActivity();
if (activity != null) {
this.activity = (MainActivity) activity;
this.session = this.activity.getSession();
}
}
// solicita-se que a classe filha seja atualizada
updateFragment();
}
De acordo com o código acima, quando o método [update] de um fragmento é executado, esse fragmento fica visível. Isso é importante porque significa que o método [Fragment.getActivity], nesse momento, retorna uma referência à atividade do aplicativo (ver parágrafo 1.10.8), o que, por sua vez, permite o acesso à sessão.
- linhas 4-10: inicializa-se a atividade e a sessão, caso ainda não tenham sido inicializadas;
- linha 12: chama-se o método [updateFragment] da classe filha. Quando este for executado, os campos [activity] e [session] aos quais ele tem acesso já terão sido inicializados;
1.11.4. A classe [PlaceholderFragment]
![]() |
A classe [PlaceholderFragment] evolui da seguinte forma:
package exemples.android;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;
// um fragmento é uma visualização exibida por um contêiner de fragmentos
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {
// componente da interface visual
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// dados
private boolean initDone;
// dados
private String text;
private int numVisit;
// nº do fragmento
private static final String ARG_SECTION_NUMBER = "section_number";
// construtor
public PlaceholderFragment() {
super();
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", "constructor");
}
}
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
...
}
// atualização do fragmento
public void updateFragment() {
...
}
}
- linha 10: a classe [PlaceholderFragment] estende a classe [AbstractFragment]. Com essa arquitetura, a criação de um fragmento consiste em:
- escrever o método [@AfterViews], que serve para inicializar o fragmento durante seu primeiro ciclo de vida ou para reinicializá-lo caso tenha havido um [onDestroyView] anteriormente. A linha 39 é obrigatória para gerenciar corretamente o ciclo de vida do fragmento;
- escrever o método [updateFragment], que atualizará o fragmento imediatamente antes de sua exibição. Esse método pode utilizar a sessão de sua classe pai;
- escrever os manipuladores de eventos do fragmento. É isso que faremos em projetos futuros;
Os métodos [@AfterViews] e [updateFragment] permanecem semelhantes ao que eram no projeto anterior:
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// registro
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
if (!initDone) {
// texto inicial
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// inicialização concluída
initDone = true;
}
// exibição do texto atual
textViewInfo.setText(text);
}
// atualização de fragmento
public void updateFragment() {
// registro
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
// incrementar número de visita
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// texto alterado
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
// informações locais para registros
protected String getLocalInfos() {
return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
numVisit, initDone, getActivity() == null);
}
- linhas 7 e 23: nos logs, exibimos as informações da classe pai com o método herdado [getParentInfos];
1.11.5. A classe [Vue1Fragment]
![]() |
A classe [Vue1Fragment] apresenta a mesma estrutura que a classe [PlaceholderFragment]:
package exemples.android;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// elementos da interface visual
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// dados
private int numVisit;
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
}
}
// gerenciador de eventos
@Click(R.id.buttonValider)
protected void doValider() {
// exibe o nome digitado
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// informações locais para logs
protected String getLocalInfos() {
return String.format("numVisit=%s", numVisit);
}
// atualização de fragmento
@Override
protected void updateFragment() {
// incremento do número de visita
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// exibe o número da visita
Toast.makeText(getActivity(), String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
- linha 9: a classe [Vue1Fragment] estende a classe [AbstractFragment];
- linhas 18-26: o método [@AfterViews] não tem nada de interessante a fazer. No entanto, é preciso defini-lo para definir o booleano [afterViewsDone] como true, pois essa informação é utilizada pela classe pai;
- linhas 42-49: o método [updateFragment] consiste em exibir uma mensagem curta mostrando o número da visita (linha 48) e incrementar esse número na sessão (linhas 44-46);
Convidamos o leitor a testar este novo projeto.
Adotaremos essa arquitetura em todos os projetos futuros:
- uma atividade e n fragmentos;
- todos os fragmentos estendem a classe [AbstractFragment];
- os dados a serem compartilhados entre fragmentos e entre fragmentos e a atividade são colocados na classe [Session];
1.11.6. Associação entre abas e fragmentos
Na classe [MainActivity], que gerencia as abas, está escrito:
// a barra de abas também está associada ao contêiner de fragmentos
// ou seja, a aba nº i exibe o fragmento nº i do contêiner
tabLayout.setupWithViewPager(mViewPager);
A linha 3 associa o gerenciador de abas ao contêiner de fragmentos. Vimos uma consequência dessa associação: quando o usuário clica na aba nº i, o contêiner de fragmentos exibe o fragmento nº i. Não vimos o inverso: quando solicitamos ao contêiner de fragmentos que exiba o fragmento nº i, a aba nº i é automaticamente selecionada.
Para ilustrar esse comportamento, vamos adicionar as opções [Fragment 1, Fragment 2, ...] ao menu atual. Quando o usuário clicar na opção [Fragment i], solicitaremos ao contêiner de fragmentos que exiba o fragmento nº i. Veremos então se a aba nº i foi selecionada ou não.
Esta etapa começa com a alteração do menu do aplicativo:
![]() | ![]() |
O conteúdo do arquivo [res / menu / menu_main.xml] evolui da seguinte forma:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment5"
android:title="@string/fragment5"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- linhas 9-28: as cinco novas opções do menu;
- os nomes das opções (linhas 10, 14, 18, 22, 26) são definidos no arquivo [res / values / strings.xml] [2]:
<resources>
<string name="app_name">Exemple-10</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- vista 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
<!-- menu -->
<string name="fragment1">Fragment 1</string>
<string name="fragment2">Fragment 2</string>
<string name="fragment3">Fragment 3</string>
<string name="fragment4">Fragment 4</string>
<string name="fragment5">Fragment 5</string>
</resources>
O resultado visual é o seguinte:
![]() |
O gerenciamento do clique nessas opções de menu é feito na classe [MainActivity]:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// processamento das opções do menu
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d("menu", "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
case R.id.fragment5: {
showFragment(4);
break;
}
}
// item processado
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// mudança do fragmento exibido
mViewPager.setCurrentItem(i);
}
}
- linha 2: o método [onOptionsItemSelected] é chamado quando ocorre um clique em uma das opções do menu;
- linha 8: obtém-se o identificador da opção clicada;
- linhas 9-36: os diferentes casos são tratados pelo método switch;
- linhas 16-36: o clique na opção [Fragment i] redireciona para o método [showFragment(i-1)] das linhas 41-45;
- linha 43: solicita-se ao contêiner de fragmentos que exiba o fragmento solicitado;
- linha 42: verifica-se previamente se isso é possível (condição 1) e se é necessário (condição 2);
O leitor é convidado a testar esta nova versão. Observa-se que, quando se solicita a exibição do fragmento nº i, este é exibido corretamente e a aba nº i é selecionada.
Agora que vimos como funciona a associação entre abas e fragmentos, vamos nos concentrar em outro caso: aquele em que o gerenciamento das abas é dissociado do gerenciamento dos fragmentos. Esse é o caso, por exemplo, quando há menos abas do que fragmentos. Para ilustrar esse novo caso de uso, vamos criar um novo projeto.
1.12. Exemplo 11: abas dissociadas dos fragmentos
1.12.1. Criação do projeto
Duplicamos o projeto [Exemple-10] em [Exemple-11]:
![]() | ![]() |
1.12.2. Objetivos
O novo aplicativo terá duas abas:
- a primeira aba exibirá sempre o fragmento [Vue1];
- a segunda aba exibirá um fragmento selecionado no menu;

- em [1], o fragmento [Vue1];
- em [2], o fragmento do tipo [PlaceholderFragment] escolhido pelo usuário;
- em [3], continua-se a contar as visitas;
1.12.3. A sessão
![]() |
A nova sessão será a seguinte:
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// número de fragmentos visitados
private int numVisit;
// n.º do fragmento do tipo [PlaceholderFragment] exibido na segunda aba
private int numFragment;
// getters e setters
...
}
- linha 10: vamos gerenciar nós mesmos o clique nas abas. Ao clicar em uma aba, é preciso recuperar o fragmento que ela exibia na última vez em que foi selecionada. O campo [numFragment] armazenará o número desse fragmento para a aba nº 2, um número contido em [0, Fragments_COUNT-2]. Quando a aba nº 2 for clicada, buscaremos na sessão o número do fragmento a ser exibido;
1.12.4. O menu
![]() |
O menu [res / menu / menu_main.xml] evolui da seguinte forma:
<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>
A aba nº 2 exibirá um dos quatro fragmentos das linhas 9 a 24. O quinto fragmento é o [Vue1Fragment], que será sempre exibido na aba nº 1.
1.12.5. A classe [MainActivity]
A classe [MainActivity] deve agora gerenciar as abas e a navegação entre elas, o que não fazia até agora. Seu código é alterado da seguinte forma:
// o gerenciador de abas
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
...
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
...
// sem rolagem
mViewPager.setScrollingEnabled(false);
// exibição da Vista1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
// inicialmente, há apenas uma aba
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// gerenciador de eventos
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// uma aba foi selecionada — alteramos o fragmento exibido pelo contêiner de fragmentos
...
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
...
}
- linha 17: o primeiro fragmento exibido pelo contêiner de fragmentos será o fragmento [Vue1Fragment]. Por definição, esse será o último fragmento do contêiner;
- linhas 20-22: como não fizemos nenhuma associação entre as abas e o contêiner de fragmentos, precisamos gerenciar as abas nós mesmos. Inicialmente, a barra de abas [tabLayout] da linha 3 não possui nenhuma aba;
- linha 20: criamos a primeira aba;
- linha 21: atribuímos um título a ela. Nos exemplos anteriores, o título das abas era o título dos fragmentos. Isso agora acabou. Por isso, removemos o método [getPageTitle] do gerenciador de fragmentos. Não precisamos mais dele:
// opcional — atribui um título aos fragmentos gerenciados
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
- linha 22: a aba criada é adicionada à barra de abas. Nossa barra de abas agora tem uma aba. O que essa aba exibe? É preciso entender que as abas e os fragmentos são dois conceitos independentes. O fragmento exibido é sempre aquele escolhido pelo contêiner de fragmentos. Se mudarmos de aba e não solicitarmos ao contêiner que altere o fragmento exibido, nada acontece: continua sendo exibido o mesmo fragmento, mas a aba selecionada mudou. Portanto, neste caso, o fragmento exibido é aquele escolhido na linha 17: o fragmento [Vue1Fragment];
- linhas 26-30: o método a ser escrito para gerenciar a mudança de aba pelo usuário;
O método [onTabSelected] das linhas 26-30 é acionado assim que há uma mudança de aba (se o usuário clicar em uma aba já selecionada, nada acontece). Seu código é o seguinte:
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (IS_DEBUG_ENABLED) {
Log.d("onglets", "onTabSelected");
}
// uma aba foi selecionada — altera-se o fragmento exibido pelo contêiner de fragmentos
// posição da aba
int position = tab.getPosition();
// nº do fragmento a ser exibido
int numFragment;
switch (position) {
case 0:
// nº do fragmento [Vue1Fragment]
numFragment = FRAGMENTS_COUNT - 1;
break;
default:
// nº do fragmento [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// exibição do fragmento
mViewPager.setCurrentItem(numFragment);
}
- linha 8: recupera-se a posição da aba em que o usuário clicou. Aqui, será obtido um número 0 ou 1;
- linhas 12-15: se for a primeira aba clicada, prepara-se a exibição do fragmento [Vue1Fragment];
- linhas 16-18: nos demais casos (aba nº 2 clicada), nos preparamos para exibir novamente o fragmento que estava sendo exibido na última vez em que a aba nº 2 foi selecionada. O número desse fragmento havia sido armazenado na sessão do aplicativo;
- linha 21: solicita-se ao contêiner de fragmentos que exiba o fragmento desejado;
Vejamos agora o gerenciamento das opções do menu (ainda em [MainActivity]):
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// registro
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// processamento das opções do menu
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d("menu", "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
}
// item processado
return true;
}
- linhas 16-31: gerenciamento das 4 opções do menu. Cada gerenciador chama o método [showFragment] com o número do fragmento a ser exibido;
O método [showFragment] é o seguinte:
// a aba nº 2
private TabLayout.Tab tab2 = null;
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// se a segunda aba ainda não existir, ela é criada
if (tab2 == null) {
tab2 = tabLayout.newTab();
tabLayout.addTab(tab2);
}
// define-se o título da segunda aba
tab2.setText(String.format("Fragment n° %s", (i + 1)));
// altera-se o fragmento exibido
mViewPager.setCurrentItem(i);
// o número do fragmento exibido é registrado na sessão
session.setNumFragment(i);
// seleciona-se a aba 2 — não faz nada se ela já estiver selecionada
tab2.select();
}
}
- lembramos que, no início da aplicação, há apenas uma aba;
- linha 2: uma referência à aba nº 2, null no início;
- linha 5: as condições de exibição não mudaram em relação à versão anterior;
- linhas 7-10: se a aba nº 2 ainda não existir, ela é criada (linha 8) e adicionada à barra de abas (linha 9);
- linha 12: insere-se no título da segunda aba o número do fragmento que será exibido, com uma numeração que começa em 1;
- linha 14: o fragmento desejado é exibido;
- linha 16: seu número é inserido na sessão;
- linha 18: a aba nº 2 é selecionada. Se ela já estivesse selecionada, nada acontecerá: o método [onTabSelected] não será executado. Se ela ainda não estivesse selecionada, o método [onTabSelected] será acionado. Esse método solicita então ao contêiner de fragmentos que exiba o fragmento já exibido na linha 14. Um simples teste no método [onTabSelected] evita esse caso:
// exibição do fragmento somente se for necessário
if (numFragment != mViewPager.getCurrentItem()) {
mViewPager.setCurrentItem(numFragment);
}
O leitor é convidado a testar esta nova versão.
1.12.6. Melhorias
Agora temos uma boa compreensão dos fragmentos, de seu ciclo de vida, do conceito de adjacência entre fragmentos e de sua relação com a barra de abas. Além disso, temos uma arquitetura robusta que acaba de passar no teste do exemplo 11:
- uma atividade e n fragmentos;
- todos os fragmentos estendem a classe [AbstractFragment];
- os dados a serem compartilhados entre fragmentos e entre fragmentos e a atividade são colocados na classe [Session];
Em um novo projeto, vamos especificar as relações entre a atividade e os fragmentos por meio da adição de uma interface.
1.13. Exemplo 12: codificar as relações entre a atividade e os fragmentos
Neste exemplo, queremos definir as relações mínimas entre a atividade e os fragmentos. Para isso, utilizaremos:
- uma interface [IMainActivity], que definirá o que os fragmentos podem solicitar à atividade;
- uma classe abstrata [AbstractFragment], que definirá o estado e os métodos que todo fragmento deve possuir;
1.13.1. Criação do projeto
Duplicamos o projeto [Exemple-11] como [Exemple-12], seguindo o procedimento descrito no parágrafo 1.4. Obtemos o seguinte resultado:
![]() | ![]() |
1.13.2. A interface [IMainActivity]
A partir dos exemplos anteriores, percebe-se que os fragmentos precisam ter acesso à sessão instanciada pela atividade. Além disso, embora não seja visível nesses exemplos, mas previsível: os manipuladores de eventos dos fragmentos às vezes terminam com uma mudança de visualização. Solicitaremos à atividade que realize essa mudança. A interface [IMainActivity] poderia, então, ser a seguinte:
![]() |
package exemples.android;
public interface IMainActivity {
// acesso à sessão
Session getSession();
// mudança de visualização
void navigateToView(int position);
// modo de depuração
boolean IS_DEBUG_ENABLED = true;
}
Na linha 12, observe-se a presença de uma constante que antes estava na classe [MainActivity]. Pretende-se reduzir o acoplamento entre os fragmentos e a atividade, limitando-o a um acoplamento entre [AbstractFragment] e [IMainActivity]. A atividade poderá, então, receber um nome diferente de [MainActivity]. Como a constante [IS_DEBUG_ENABLED] é utilizada nos fragmentos, ela é transferida para a interface [IMainActivity].
1.13.3. A classe abstrata [AbstractFragment]
A classe abstrata [AbstractFragment] sofre pouquíssimas alterações:
// dados acessíveis às classes filhas
protected boolean afterViewsDone = false;
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// atividade
protected IMainActivity mainActivity;
protected Activity activity;
...
// atualização de fragmento
protected void update() {
// recuperamos a atividade e a sessão
if (mainActivity == null) {
this.activity = getActivity();
if (this.activity != null) {
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
}
}
// solicita-se que a classe filha seja atualizada
updateFragment();
}
- linhas 6 e 7: mantêm-se dois tipos de referência à atividade:
- linha 6: uma referência à atividade que implementa a interface [IMainActivity];
- linha 7: uma referência à atividade que herda da classe Android [Activity]. Esse é o caso de todas as atividades;
Essas duas referências apontam, obviamente, para o mesmo objeto. Mas esse objeto é visto com dois tipos diferentes. Isso nos evitará conversões de tipo durante a execução;
- linha 14: recuperamos uma referência à atividade por meio do método [getActivity];
- linha 15: se essa referência for diferente de nulo, então podemos acessar a sessão;
- linhas 16-17: armazenamos a atividade como implementadora da interface [IMainActivity] e a sessão;
1.13.4. Modificação do gerenciador de fragmentos
O gerenciador de fragmentos [SectionsPagerAdapter] na classe [MainActivity] é modificado em um único ponto: em vez de gerenciar fragmentos do tipo [Fragment], ele agora gerencia fragmentos do tipo [AbstractFragment]:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// os fragmentos
private AbstractFragment[] fragments;
// nº do fragmento
private static final String ARG_SECTION_NUMBER = "section_number";
// construtor
public SectionsPagerAdapter(FragmentManager fm) {
// pai
super(fm);
// inicialização da matriz de fragmentos
fragments = new AbstractFragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
...
}
// um fragmento de +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
// nº do fragmento na posição
@Override
public AbstractFragment getItem(int position) {
...
}
// retorna o número de fragmentos gerenciados
@Override
public int getCount() {
...
}
}
1.13.5. Alteração da classe [MainActivity]
A classe [MainActivity] deve implementar a interface [IMainActivity]:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{
...
// injeção de sessão
@Bean(Session.class)
protected Session session;
...
// getter de sessão
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// exibe a visualização da posição
if(mViewPager.getCurrentItem()!=position){
// exibição do fragmento
mViewPager.setCurrentItem(position);
}
}
- linhas 10-12: o método [getSession] já existia;
- linhas 15-22: o método [navigateToView] exibe o fragmento nº [position];
- linha 17: verifica-se se há algo a ser feito;
- linha 19: o fragmento nº [position] é exibido;
Nesta etapa, execute o aplicativo. Ele deve funcionar.
1.13.6. Alteração da exibição dos fragmentos em [MainActivity]
Atualmente, a classe [MainActivity] exibe um fragmento por meio da instrução:
// exibição da Vista1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
Como o método [navigateToView] faz o mesmo, substitua esse tipo de instrução em todos os locais (2 locais) por:
Em seguida, execute o aplicativo. Ele deve continuar funcionando normalmente.
1.13.7. Conclusão
A partir de agora, sempre utilizaremos a arquitetura anterior:
- uma atividade que implemente a interface [IMainActivity];
- fragmentos que estendem a classe [AbstractFragment], o que os obriga a implementar o método [updateFragment]. Esses fragmentos também devem possuir um método [@AfterViews], no qual definem o booleano [afterViewsDone] como true;
- uma sessão que encapsule os dados a serem compartilhados entre fragmentos e a atividade;
1.14. Exemplo-13: Exemplo-05 com fragmentos
No projeto [Exemple-05], introduzimos a navegação entre visualizações. Na ocasião, tratava-se de uma navegação entre atividades: 1 visualização = 1 atividade. Propomos aqui ter uma única atividade com várias visualizações do tipo [AbstractFragment].
1.14.1. Criação do projeto
Duplicamos o projeto anterior [Exemple-12] em [Exemple-13], seguindo o procedimento descrito no parágrafo 1.4. Obtemos o seguinte resultado:
![]() | ![]() |
1.14.2. Estruturação do projeto
Começaremos a utilizar pacotes para organizar o código. Por enquanto, podemos distinguir duas áreas distintas:
- o gerenciamento da atividade;
- a gestão dos fragmentos;
Criamos para elas dois pacotes, [exemples.android.activity] e [exemples.android.fragments]:
![]() |
![]() | ![]() |
Fazemos o mesmo para criar o pacote [exemples.android.fragments]:
![]() | ![]() |
No [8], criamos um terceiro pacote chamado [architecture], no qual colocaremos as entidades [IMainActivity, AbstractFragment, Session, MyPager], que são os elementos básicos da arquitetura do nosso aplicativo. Isso serve para nos lembrar que fizemos uma escolha específica de arquitetura. Em seguida, mova os elementos existentes do projeto conforme indicado em [9]. Cada movimentação deve ser validada clicando no botão [Refactor].
Nesta etapa, compile a aplicação. Temos os seguintes erros em [MainActivity:
![]() |
Ao mover as classes para os pacotes, o Android Studio fez as alterações necessárias nos códigos do aplicativo (linhas 18 a 21, por exemplo). As classes mencionadas nas linhas 15 e 17 não foram movidas. Elas são geradas pela biblioteca Android Annotations. Para essas classes, é necessário alterar manualmente os imports. Assim, essas linhas ficam da seguinte forma:
![]() |
Feito isso, não há mais erros de compilação. Execute o aplicativo. Aparece então o seguinte erro:
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{exemples.android/exemples.android.MainActivity_}:
Esse erro se origina do manifesto do aplicativo:
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
As linhas 3 e 12 indicam que a atividade designada é [exemples.android.MainActivity_]. No entanto, como a atividade foi migrada para o pacote [activity], a linha 12 deve agora ser:
android:name=".activity.MainActivity_"
Preste atenção ao ponto (.) antes de [activity]. Mais uma vez, o Android Studio não conseguiu atualizar o manifesto porque este faz referência a uma classe Android Annotations que não foi movida. O uso da biblioteca AA traz, portanto, uma série de inconvenientes.
1.14.3. Limpeza do projeto
No novo projeto:
- não há mais abas, botões flutuantes nem menus;
- os fragmentos [PlaceholderFragment] desaparecem. O aplicativo passará a gerenciar dois fragmentos: o [Vue1Fragment], que já existe, e o [Vue2Fragment], que precisará ser criado;
- a sessão não é mais a mesma;
1.14.3.1. Limpeza dos fragmentos
Exclua as classes [PlaceHolderFragment] e [1]:
![]() | ![]() |
Da mesma forma, exclua a visualização [res / layout / fragment_main.xml] associada a este fragmento [2].
1.14.3.2. Limpeza da sessão
A sessão está atualmente da seguinte forma:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// número de fragmentos visitados
private int numVisit;
// n.º do fragmento do tipo [PlaceholderFragment] exibido na segunda aba
private int numFragment;
// getters e setters
public int getNumVisit() {
return numVisit;
}
public void setNumVisit(int numVisit) {
this.numVisit = numVisit;
}
public int getNumFragment() {
return numFragment;
}
public void setNumFragment(int numFragment) {
this.numFragment = numFragment;
}
}
Não mantemos nada dessa sessão.
Compile o projeto. As linhas com erros são aquelas que utilizavam o conteúdo da sessão. Exclua-as. Na classe [Vue1Fragment], também excluímos a variável [numVisit] do código, que passa a ser o seguinte:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// elementos da interface visual
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gerenciador de eventos
@Click(R.id.buttonValider)
protected void doValider() {
// exibe-se o nome digitado
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// atualização do fragmento
@Override
protected void updateFragment() {
}
}
1.14.3.3. Remoção das abas, do botão flutuante e do menu
A remoção das abas e do botão flutuante é feita em dois locais:
- na visualização [res / layout / activity-main.xml], que define esses elementos e sua localização na visualização;
- no código da atividade [MainActivity];
A remoção do menu também é feita em dois locais:
- na vista [res / menu / menu-main.xml], que define as opções do menu;
- no código da atividade [MainActivity];
O código da visualização [res / layout / activity-main.xml] é atualmente o 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.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<exemples.android.architecture.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
- excluímos as linhas [28-31, 41-47];
- elimine também a barra de ferramentas das linhas 18 a 24;
O código do menu [res / menu / menu_main.xml] é atualmente 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/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>
- removeremos as linhas 9 a 24. Assim, deixamos uma opção que não será utilizada. Apenas para ter um exemplo de declaração de uma opção de menu que poderá ser reproduzida por copiar/colar;
Na classe [MainActivity], exclua tudo o que se refira às abas, ao botão flutuante, à barra de ferramentas e ao menu. Para localizar essas referências, o mais simples é excluir suas declarações:
// o gerenciador de abas
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// o botão flutuante
@ViewById(R.id.fab)
protected FloatingActionButton fab;
e recompilar o aplicativo. As linhas com erros são aquelas que fazem referência aos elementos removidos. Exclua, então, todas essas linhas. Além disso, altere o gerenciador de fragmentos para que ele não faça mais referência ao fragmento [PlaceholderFragment] que removemos:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// os fragmentos
private AbstractFragment[] fragments;
// construtor
public SectionsPagerAdapter(FragmentManager fm) {
// pai
super(fm);
}
// posição do fragmento
@Override
public AbstractFragment getItem(int position) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// retorna o número de fragmentos gerenciados
@Override
public int getCount() {
return fragments.length;
}
}
- linhas 7-10: eliminamos toda a geração de fragmentos;
Nesta fase, não deve haver mais erros de compilação. Na classe [MainActivity], chegamos ao seguinte código intermediário:
package exemples.android.activity;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.IMainActivity;
import exemples.android.architecture.MyPager;
import exemples.android.architecture.Session;
import exemples.android.fragments.Vue1Fragment_;
import org.androidannotations.annotations.*;
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
// o contêiner de fragmentos
@ViewById(R.id.container)
protected MyPager mViewPager;
// a barra de ferramentas
@ViewById(R.id.toolbar)
protected Toolbar toolbar;
// sessão de injeção
@Bean(Session.class)
protected Session session;
// número de fragmentos
private final int FRAGMENTS_COUNT = 5;
// adjacência dos fragmentos
private final int OFF_SCREEN_PAGE_LIMIT = 2;
// modo de depuração
public static final boolean IS_DEBUG_ENABLED = true;
// o gerenciador de fragmentos
private SectionsPagerAdapter mSectionsPagerAdapter;
// construtor
public MainActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "constructor");
}
}
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
// barra de ferramentas — é onde o nome do aplicativo é exibido
setSupportActionBar(toolbar);
// o 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.setAdapter(mSectionsPagerAdapter);
// deslocamento dos fragmentos
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// o deslize entre fragmentos é desativado
mViewPager.setSwipeEnabled(false);
// sem rolagem
mViewPager.setScrollingEnabled(false);
// exibição Vista1
navigateToView(FRAGMENTS_COUNT - 1);
}
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
}
// getter de sessão
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// exibindo a vista “posição”
if (mViewPager.getCurrentItem() != position) {
// exibição do fragmento
mViewPager.setCurrentItem(position);
}
}
// o gerenciador de fragmentos
// é a ele que se solicita os fragmentos a serem exibidos na visualização principal
// deve definir os métodos [getItem] e [getCount] — os demais são opcionais
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// os fragmentos
private AbstractFragment[] fragments;
// construtor
public SectionsPagerAdapter(FragmentManager fm) {
// pai
super(fm);
}
// n.º de posição do fragmento
@Override
public AbstractFragment getItem(int position) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// retorna o número de fragmentos gerenciados
@Override
public int getCount() {
return fragments.length;
}
}
}
Ainda há algumas alterações a serem feitas:
- exclua a linha 31, que já não tem mais razão de ser;
- linha 33: defina 1 como adjacência de fragmentos;
- linha 76: navegue até a visualização 0. Será ela que será exibida primeiro;
- linha 108: inicialize a matriz com o fragmento [Vue1Fragment_]:
// os fragmentos
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
Portanto, temos apenas um fragmento. Execute o aplicativo. Você deve obter o seguinte resultado:

O botão [Valider] deve funcionar.
1.14.4. Criação dos fragmentos e das visualizações associadas
O aplicativo terá duas visualizações, as do projeto [Exemple-05]. Já temos a visualização [vue1.xml] no projeto atual. Agora, vamos duplicar [vue2.xml] de [Exemple-05] para [Exemple-12] (abra os dois projetos e faça copiar/colar) entre eles.
![]() | ![]() |
- em [1], a nova visualização. Ao tentar editá-la, aparecem erros no [2]. Precisamos modificar o arquivo [strings.xml] e [3] para adicionar as cadeias de caracteres referenciadas por essa nova visualização:
<resources>
<string name="app_name">Exemple-13</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- visualização 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<!-- visualização 2 -->
<string name="btn_vue2">Vue n° 2</string>
<string name="titre_vue2">Vue n° 2</string>
<string name="btn_vue1">Vue n° 1</string>
</resources>
Duplicamos a classe [Vue1Fragment] em [Vue2Fragment]:
![]() |
e modificamos o código copiado da seguinte maneira:
package exemples.android.fragments;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// atualização do fragmento
@Override
protected void updateFragment() {
}
}
- linha 9: o fragmento está associado à visualização [res / layout / vue2.xml];
- linha 10: a classe estende a classe abstrata [AbstractFragment];
- linhas 12-20: o método [@AfterViews] é obrigatório;
- linhas 23-25: o método [updateFragment] é obrigatório;
1.14.5. Implementação dos fragmentos e da navegação entre eles
A atividade passará a gerenciar dois fragmentos. Sua classe [SectionsPagerAdapter] é alterada da seguinte forma:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// os fragmentos
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
}
A interface [IMainActivity] garante a navegação entre visualizações com seu método [navigateToView]. Vamos tratar o clique no botão [Vue n° 2] do fragmento [Vue1Fragment]:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// elementos da interface visual
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gerenciadores 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();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
mainActivity.navigateToView(1);
}
// atualização do fragmento
@Override
protected void updateFragment() {
}
}
- linhas 37-40: o método [showVue2] lida com o evento “clique” no botão [Vue n° 2];
- linha 39: a navegação é feita com o método [navigateToView] da atividade. Vale lembrar que a atividade foi armazenada na classe pai da seguinte forma:
// atividade
protected IMainActivity mainActivity;
e que essa atividade já foi inicializada ao se chegar a qualquer gerenciador de eventos.
- linha 34: a instrução utiliza a variável [activity] da classe pai, que é uma referência à atividade como instância do tipo Android [Activity];
protected Activity activity;
Encontramos um código semelhante para o fragmento [Vue2Fragment]:
package exemples.android.fragments;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gerenciadores de eventos ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// atualização de fragmento
@Override
protected void updateFragment() {
}
}
- linhas 24-27: o método [showVue1] lida com o evento “clique” no botão [Vue n° 1];
Execute o projeto e verifique se a navegação entre as visualizações funciona.
1.14.6. Definição da sessão
O funcionamento do aplicativo é o seguinte:
- digitação de um nome na visualização nº 1;
- exibição desse nome na visualização nº 2;
Para que a visualização nº 1 possa transmitir o nome inserido à visualização nº 2, utilizaremos a seguinte sessão;
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// nome
private String nom;
// getters e setters
...
}
- linha 8: o nome inserido;
A classe [MainActivity] inicializará a sessão da seguinte maneira:
// injeção de sessão
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// inicialização da sessão
session.setNom("");
}
1.14.7. Gravação final dos fragmentos
No fragmento [Vue1Fragment], modificamos o código do manipulador de clique no botão [Valider]:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// elementos da interface visual
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
...
// manipuladores de eventos ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// o nome digitado é armazenado
String nom = editTextNom.getText().toString();
// exibe-se
Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
// o nome digitado é inserido na sessão
session.setNom(editTextNom.getText().toString());
// navega-se para a visualização nº 2
mainActivity.navigateToView(1);
}
// atualização do fragmento
@Override
protected void updateFragment() {
}
}
- linhas: 31-37: gerenciamos o clique no botão [Vue n° 2];
- linha 34: antes de navegar para a visualização nº 2, inserimos o nome digitado na sessão para que a nova visualização tenha acesso a ele;
A visualização [Vue2Fragment] é alterada da seguinte forma:
package exemples.android.fragments;
import android.util.Log;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
// componentes da interface visual
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gerenciadores de eventos ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// atualização de fragmento
@Override
protected void updateFragment() {
// recupera-se o nome digitado na sessão
String nom = session.getNom();
// exibindo-o
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
Quando a visualização nº 2 for exibida, é necessário exibir o nome inserido na visualização nº 1. Sabemos que, logo após sua exibição, seu método [updateFragment] será executado. Portanto, é nesse método (linhas 36-42) que podemos inserir o código para exibir o nome.
- linhas 16-17: declaração do único componente visual da visualização;
- linha 39: o nome inserido na visualização nº 1 é recuperado da sessão;
- linha 41: o rótulo [textViewBonjour] é alterado;
Execute o projeto e verifique se ele funciona.
1.14.8. Gerenciamento do ciclo de vida dos fragmentos
No fragmento [Vue1Fragment], o método [@AfterViews] é o seguinte:
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// registro
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
Esse método está incompleto. De fato, é preciso sempre considerar o caso em que o fragmento é reciclado após uma operação [onDestroyView]. Nesse caso, a visualização do fragmento 1 é regenerada e o nome que possa ter sido inserido anteriormente desaparecerá da visualização. Não queremos que isso aconteça. Atualmente, o nome inserido permanece exibido porque a adjacência dos fragmentos de 1 faz com que o ciclo de vida do fragmento [Vue1Fragment] seja executado apenas uma vez. No entanto, é preferível prever o caso da reutilização do fragmento.
Existem várias maneiras de resolver esse problema:
- pode-se aproveitar o fato de que o método [update] é executado sistematicamente a cada exibição do fragmento para atualizar o nome digitado;
- pode-se fazer essa atualização apenas quando o método [@AfterViews] for reexecutado. É essa última opção que adotamos;
Modificamos o código de [Vue1Fragment] da seguinte maneira:
// elementos da interface visual
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// dados
private String nom;
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// (re)inicializa o texto exibido
editTextNom.setText(nom);
}
// gerenciadores de eventos ----------------------------------
...
@Click(R.id.buttonVue2)
protected void showVue2() {
// registra-se o nome digitado para poder recuperá-lo caso o fragmento seja reciclado
nom = editTextNom.getText().toString();
// o nome digitado é inserido na sessão
session.setNom(nom);
// navega-se para a visualização nº 2
activity.navigateToView(1);
}
- linha 27: ao sair da visualização 1 para a visualização 2, o nome inserido é memorizado;
- linha 17: a cada nova execução do ciclo de vida do fragmento, o último nome inserido é exibido novamente;
Para o fragmento [Vue2Fragment], o código existente é suficiente:
// componentes da interface visual
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// atualização do fragmento
@Override
protected void updateFragment() {
// recupera-se o nome digitado na sessão
String nom = session.getNom();
// exibe-se
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
- o único componente visual da visualização (linha 3) é atualizado sempre que a visualização é exibida (linha 21). O método [@AfterViews], portanto, não tem nada a acrescentar;
1.14.9. Conclusão
Nesse ponto, demonstramos mais uma vez a relevância de nossa arquitetura:
- uma atividade que implementa a interface [IMainActivity];
- fragmentos que estendem a classe [AbstractFragment], o que os obriga a implementar o método [updateFragment]. Esses fragmentos também devem possuir um método [@AfterViews], no qual definem o valor booleano [afterViewsDone] como true;
- uma sessão que encapsule os dados a serem compartilhados entre fragmentos e atividades;
1.15. Exemplo 14: uma arquitetura de duas camadas
Vamos construir um aplicativo de uma única visualização com a seguinte arquitetura:
![]() |
1.15.1. Criação do projeto
Duplicamos o projeto anterior [Exemple-12] em [Exemple-13], seguindo o procedimento descrito no parágrafo 1.4. Obtemos o seguinte resultado:
![]() | ![]() |
1.15.2. A visualização [vue1]
O aplicativo terá apenas a visualização [vue1.xml]. Portanto, vamos excluir a outra visualização, [vue2.xml], bem como seu fragmento associado:
![]() | ![]() |
Compile o aplicativo. Aparecem erros em [MainActivity]:
![]() |
Corrija a linha 4 abaixo no gerenciador de fragmentos [SectionsPagerAdapter]
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// os fragmentos
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
A linha 4 acima passa a ser:
// os fragmentos
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
Exclua as importações que se tornaram desnecessárias: [Ctrl-Shift-O]. Não deve haver mais erros de compilação. Execute o projeto: a visualização nº 1 deve aparecer. Agora vamos modificá-la.
Vamos criar a visualização [vue1.xml], que permitirá gerar números aleatórios:
![]() |
Seus componentes são os seguintes:
Seu código XML é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="20dp"
android:orientation="vertical" >
<TextView
android:id="@+id/txt_Titre2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/txt_nbaleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_Titre2"
android:layout_marginTop="20dp"
android:text="@string/txt_nbaleas" />
<EditText
android:id="@+id/edt_nbaleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_nbaleas"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_nbaleas"
android:inputType="number" />
<TextView
android:id="@+id/txt_errorNbAleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edt_nbaleas"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_nbaleas"
android:text="@string/txt_errorNbAleas"
android:textColor="@color/red" />
<TextView
android:id="@+id/txt_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_nbaleas"
android:layout_marginTop="20dp"
android:text="@string/txt_a" />
<EditText
android:id="@+id/edt_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_a"
android:inputType="number" />
<TextView
android:id="@+id/txt_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_a"
android:text="@string/txt_b" />
<EditText
android:id="@+id/edt_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_b"
android:inputType="number" />
<TextView
android:id="@+id/txt_errorIntervalle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edt_b"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_b"
android:text="@string/txt_errorIntervalle"
android:textColor="@color/red" />
<Button
android:id="@+id/btn_Executer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_a"
android:layout_marginTop="20dp"
android:text="@string/btn_executer" />
<TextView
android:id="@+id/txt_Reponses"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btn_Executer"
android:layout_marginTop="30dp"
android:text="@string/list_reponses"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/blue" />
<ListView
android:id="@+id/lst_reponses"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_Reponses"
android:layout_marginTop="40dp"
android:background="@color/wheat"
android:clickable="true"
tools:listitem="@android:layout/simple_list_item_1" >
</ListView>
</RelativeLayout>
A visualização anterior utiliza rótulos definidos no arquivo [res / values / strings.xml]:
<resources>
<string name="app_name">Exemple-14</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- visualização 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="list_reponses">Liste des réponses</string>
<string name="btn_executer">Exécuter</string>
<string name="aleas">Génération de N nombres aléatoires</string>
<string name="txt_nbaleas">Valeur de N :</string>
<string name="txt_a">"Intervalle [a,b] de génération, a : "</string>
<string name="txt_b">"b : "</string>
<string name="txt_dummy">Dummy</string>
<string name="txt_errorNbAleas">Tapez un nombre entier >=1</string>
<string name="txt_errorIntervalle">Les bornes de l\'intervalle doivent être entières et b>=a</string>
</resources>
As cores utilizadas no [vue1.xml] estão definidas no arquivo [res / values / colors.xml]:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<!-- cores do aplicativo -->
<color name="red">#FF0000</color>
<color name="blue">#0000FF</color>
<color name="wheat">#FFEFD5</color>
<color name="floral_white">#FFFAF0</color>
</resources>
1.15.3. A sessão
![]() |
Como, neste caso, há apenas um fragmento, não é necessário prever comunicação entre fragmentos. A sessão, portanto, estará vazia:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
}
Nesta etapa, compile o aplicativo. Erros aparecerão nas linhas que utilizavam elementos da sessão, agora vazia. Exclua essas linhas e verifique se a compilação não apresenta mais erros.
1.15.4. O fragmento [Vue1Fragment]
![]() |
Modificamos o fragmento [Vue1Fragment] existente da seguinte maneira:
package exemples.android.fragments;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// os elementos da interface visual
@ViewById(R.id.lst_reponses)
protected ListView listReponses;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNbAleas;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
protected TextView txtErrorIntervalle;
// lista de respostas a um comando
private List<String> reponses = new ArrayList<>();
// adaptador da listview
private ArrayAdapter<String> adapterReponses;
// as entradas
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// oculta as mensagens de erro
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
}
@Click(R.id.btn_Executer)
void doExecuter() {
// ocultam-se as eventuais mensagens de erro anteriores
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// verifica-se a validade dos dados inseridos
if (!isPageValid()) {
return;
}
}
// verifica-se a validade dos dados inseridos
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// registro
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
}
}
- Aqui há apenas um fragmento cujo ciclo de vida será executado apenas uma única vez, no início da aplicação. Por esse motivo, os métodos [@AfterViews] (linhas 46-57) e [udateFragment] (linhas 75-81) serão executados apenas uma vez ao iniciar o aplicativo;
- linhas 55-56: ocultamos as duas mensagens de erro da visualização (representadas abaixo) [1-2];
![]() |
- linhas 59-60: o método executado ao clicar no botão [Exécuter];
- linhas 71-73: verifica-se a validade dos dados inseridos;
O método [isPageValid] é o seguinte:
// as entradas
private int nbAleas;
private int a;
private int b;
...
// verifica-se a validade dos dados inseridos
private boolean isPageValid() {
// inserção do número de números aleatórios
nbAleas = 0;
Boolean erreur;
int nbErreurs = 0;
try {
nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
erreur = (nbAleas < 1);
} catch (Exception ex) {
erreur = true;
}
// erro?
if (erreur) {
nbErreurs++;
txtErrorAleas.setVisibility(View.VISIBLE);
}
// inserção de a
a = 0;
erreur = false;
try {
a = Integer.parseInt(edtA.getText().toString());
} catch (Exception ex) {
erreur = true;
}
// erro?
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// inserção de b
b = 0;
erreur = false;
try {
b = Integer.parseInt(edtB.getText().toString());
erreur = b < a;
} catch (Exception ex) {
erreur = true;
}
// erro?
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// voltar
return (nbErreurs == 0);
}
- linhas 2-4: esses três campos são inicializados pelo método [isPageValid]. Além disso, esse método retorna true se todas as entradas forem válidas; caso contrário, retorna false. Se houver entradas inválidas, as mensagens de erro correspondentes são exibidas;
Nesta fase, o aplicativo está pronto para execução. Verifique o funcionamento do método [isPageValid] inserindo dados incorretos.
1.15.5. A camada [métier]
![]() |
![]() |
A camada [métier] apresenta a seguinte interface [IMetier]:
package exemples.android.metier;
import java.util.List;
public interface IMetier {
List<Object> getAleas(int a, int b, int n);
}
O método [getAleas(a,b,n)] normalmente retorna n números inteiros aleatórios no intervalo [a,b]. Também foi previsto que ela retorne uma exceção uma vez a cada três chamadas; essa exceção também é incluída nas respostas retornadas pelo método. No final, o método retorna uma lista de objetos do tipo [Exception] ou [Integer].
A implementação [Metier] dessa interface é a seguinte:
package exemples.android.metier;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@EBean(scope = EBean.Scope.Singleton)
public class Metier implements IMetier {
public List<Object> getAleas(int a, int b, int n) {
// a lista de objetos
List<Object> réponses = new ArrayList<Object>();
// algumas verificações
if (n < 1) {
réponses.add(new AleaException("Le nombre d'entier aléatoires demandé doit être supérieur ou égal à 1"));
}
if (a < 0) {
réponses.add(new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0"));
}
if (b < 0) {
réponses.add(new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0"));
}
if (a >= b) {
réponses.add(new AleaException("Dans l'intervalle [a,b], on doit avoir a< b"));
}
// erro?
if (réponses.size() != 0) {
return réponses;
}
// gerando números aleatórios
Random random = new Random();
for (int i = 0; i < n; i++) {
// gera-se uma exceção aleatória 1 vez a cada 3
int nombre = random.nextInt(3);
if (nombre == 0) {
réponses.add(new AleaException("Exception aléatoire"));
} else {
// caso contrário, retorna-se um número aleatório entre dois limites [a,b]
réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
}
}
// resultado
return réponses;
}
}
- linha 9: utiliza-se a anotação AA [@EBean] na classe [Metier] para poder inserir referências desta última na camada [Présentation]. O atributo (scope = EBean.Scope.Singleton) faz com que a classe [Metier] seja instanciada apenas uma vez. Portanto, é sempre a mesma referência que é injetada, mesmo que seja injetada várias vezes na camada [Présentation];
- o restante do código é padrão;
O tipo [AleaException] utilizado pela classe [Metier] é o seguinte:
package exemples.android.metier;
public class AleaException extends RuntimeException {
private static final long serialVersionUID = 1L;
public AleaException() {
}
public AleaException(String detailMessage) {
super(detailMessage);
}
public AleaException(Throwable throwable) {
super(throwable);
}
public AleaException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
}
- linha 3: a classe [AleaException] estende a classe do sistema [RuntimeException], o que a torna uma exceção não controlada: não é necessário tratá-la em um try/catch, nem incluí-la na assinatura dos métodos;
1.15.6. A atividade [MainActivity] revisitada
![]() |
Camada
[metier]
Atividade
Visão
Usuário
A atividade implementará a interface [IMetier] da camada [métier]. Assim, o fragmento/visualização terá apenas a atividade como interlocutor.
A atividade [MainActivity] já implementa a interface [IMainActivity]. Para que ela também implemente a interface [IMetier], é possível:
- adicionar a interface [IMetier] às interfaces implementadas pela atividade;
- fazer com que a interface [IMainActivity], por sua vez, estenda a interface [IMetier]. É essa a abordagem que adotamos;
A interface [IMainActivity] passa a ser a seguinte:
![]() |
package exemples.android.architecture;
import exemples.android.metier.IMetier;
public interface IMainActivity extends IMetier {
// acesso à sessão
Session getSession();
// mudança de visualização
void navigateToView(int position);
// modo de depuração
public static final boolean IS_DEBUG_ENABLED = true;
}
- linha 5: a interface [IMainActivity] estende a interface [IMetier]
A classe [MainActivity] passa a ter a seguinte forma:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
...
// injeção de sessão
@Bean(Session.class)
protected Session session;
// injeção de lógica de negócio
@Bean(Metier.class)
protected IMetier metier;
...
// implementação IMetier --------------------------------------------------------------------
@Override
public List<Object> getAleas(int a, int b, int n) {
return metier.getAleas(a, b, n);
}
- linhas 11-12: a camada [métier] é injetada na atividade. Para isso, utiliza-se a anotação AA [@Bean], cujo parâmetro é a classe que possui a anotação AA [@EBean];
- linha 2: a atividade implementa a interface [IMainActivity] e, portanto, a interface [IMetier] da camada [métier];
- linhas 16-19: implementação do único método da interface [IMetier]. Basta delegar a chamada à camada [métier];
1.15.7. O fragmento [Vue1Fragment] revisitado
![]() |
O código da classe [Vue1Fragment] sofre as seguintes alterações:
package exemples.android.fragments;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// elementos da interface visual
@ViewById(R.id.lst_reponses)
protected ListView listReponses;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNbAleas;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
protected TextView txtErrorIntervalle;
// lista de respostas a um comando
private List<String> reponses = new ArrayList<>();
// adaptador da listview
private ArrayAdapter<String> adapterReponses;
// as entradas
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
...
}
@Click(R.id.btn_Executer)
void doExecuter() {
...
}
// verifica-se a validade dos dados inseridos
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
// será executado apenas uma vez ao iniciar o aplicativo
// cria-se o adaptador do ListView — para isso, é necessário que a variável [activity] tenha sido inicializada
adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
}
- linhas 69-70: define-se o adaptador do componente do tipo [ListView];
O componente [ListView] serve para exibir uma lista de elementos. Ele faz isso por meio de um adaptador do tipo [ListAdapter], que, por sua vez, está conectado à fonte de dados que deve alimentar o [ListView]. Para definir o adaptador de um [ListView], está disponível o seguinte método [ListView.setAdapter]:
public void setAdapter (ListAdapter adapter)
[ListAdapter] é uma interface. A classe [ArrayAdapter] é uma classe que implementa essa interface. O construtor utilizado na linha 69 acima é o seguinte:
- [context] é a atividade que exibe o [ListView];
- [resource] é o número inteiro que identifica a visualização utilizada para exibir um elemento do [ListView]. Essa visualização pode ter qualquer grau de complexidade. É o desenvolvedor quem a constrói de acordo com suas necessidades;
- [textViewResourceId] é o número inteiro que identifica um componente [TextView] na visualização [resource]. A sequência de caracteres será exibida por esse componente;
- [objects]: a lista de objetos exibidos pelo [ListView]. O método [toString] dos objetos é utilizado para exibir o objeto na [TextView] identificado por [textViewResourceId] na visualização identificada por [resource].
A tarefa do desenvolvedor é criar a vista [resource] que exibirá cada elemento do [ListView]. Para o caso simples em que se deseja exibir apenas uma sequência de caracteres, como aqui, o Android fornece a vista identificada por [android.R.layout.simple_list_item_1]. Ela contém um componente [TextView] identificado por [android.R.id.text1]. Esse é o método utilizado na linha 69 para criar o adaptador do [ListView]. Esse adaptador precisa ser definido apenas uma vez. Para permitir sua reutilização, ele foi definido como uma variável de instância da classe (linha 39). Vamos dar uma olhada novamente na linha 69:
adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
O primeiro parâmetro do construtor [ArrayAdapter] é a atividade obtida em um fragmento pelo [getActivity] e que foi armazenada aqui na variável [activity] da classe pai. Esse campo nem sempre possui um valor. Assim, os logs mostram que, ao chegar ao método [@AfterViews], ele ainda não foi inicializado e, portanto, não é possível inserir as linhas 69-70 nesse método. No método [updateFragment], isso é possível, pois sabemos que, quando esse método é executado, necessariamente já existe o [activity!=null]. O adaptador está aqui associado à fonte de dados [reponses], definida na linha 37;
O método [doExecuter] processa o clique no botão [Exécuter]. Seu código é o seguinte:
@Click(R.id.btn_Executer)
void doExecuter() {
// ocultam-se eventuais mensagens de erro anteriores
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// apaga-se as respostas anteriores
reponses.clear();
adapterReponses.notifyDataSetChanged();
// verifica-se a validade dos dados inseridos
if (!isPageValid()) {
return;
}
// solicita-se os números aleatórios à atividade
List<Object> data = mainActivity.getAleas(a, b, nbAleas);
// cria-se uma lista de strings a partir desses dados
for (Object o : data) {
if (o instanceof Exception) {
reponses.add(((Exception) o).getMessage());
} else {
reponses.add(o.toString());
}
}
// atualiza a ListView
adapterReponses.notifyDataSetChanged();
}
- linhas 7-8: deseja-se esvaziar o ListView. Para isso, esvazia-se a fonte de dados [reponses] e solicita-se que o adaptador associado ao ListView seja atualizado;
- linhas 10-12: antes de executar a ação solicitada, verifica-se se os valores inseridos estão corretos;
- linha 14: solicita-se à atividade a lista de números aleatórios. Obtém-se uma lista de objetos em que cada objeto é do tipo [Integer] ou [AleaException];
- linhas 16-22: a partir da lista de objetos obtida, atualiza-se a fonte de dados [reponses] que o ListView exibe;
- linha 24: solicita-se que o adaptador do ListView seja atualizado;
1.15.8. Execução
Execute o projeto e verifique se ele está funcionando corretamente.
1.16. Exemplo 15: arquitetura cliente/servidor
Abordaremos uma arquitetura comum para um aplicativo Android, aquela em que o aplicativo Android se comunica com serviços web remotos. Teremos agora a seguinte arquitetura:
![]() |
Adicionamos à aplicação Android uma camada [DAO] para se comunicar com o servidor remoto. Ela se comunicará com o servidor que gera os números aleatórios exibidos pelo tablet Android. Esse servidor terá a seguinte arquitetura de duas camadas:
![]() |
Os clientes consultam certos URL da camada [web / jSON] e recebem uma resposta de texto no formato jSON (JavaScript Object Notation). Aqui, nosso serviço web processará uma única URL do tipo [/a/b], que retornará um número aleatório no intervalo [a,b]. Descreveremos a aplicação na seguinte ordem:
O servidor
- sua camada [métier];
- seu serviço [web / jSON] implementado com Spring MVC;
O cliente
- sua camada [DAO]. Não haverá camada [métier];
1.16.1. O servidor [web / jSON]
Queremos construir a seguinte arquitetura:
![]() |
1.16.1.1. Criação do projeto
Vamos construir o serviço web com o ecossistema Spring [http://spring.io/]. Acessamos o site [http://start.spring.io/] (junho de 2016), que nos permitirá gerar um projeto Gradle com as dependências necessárias para o nosso projeto, que não é um projeto Android e para cuja construção o Android Studio não oferece nenhuma ajuda:
![]() |
- em [1]: escolha um projeto Gradle;
- em [2-3]: as características da dependência jar gerada pelo projeto (veja abaixo);
- em [4]: selecione a dependência web [5] para que os binários necessários ao nosso serviço web estejam disponíveis;
- em [6]: gere o projeto. Um arquivo zip de um projeto Gradle de estrutura básica é então gerado e disponibilizado para download;
O que colocar em [2-3]? Já utilizamos dependências do Gradle. A do projeto anterior, por exemplo, era a seguinte:
![]() |
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
...
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
- linha 22: uma dependência tem o formato [groupId:artifactId:version]. O que é solicitado no formulário do site [http://start.spring.io/]:
- em [2] é [groupId];
- em [3] é [artifactId];
Descompacte, na pasta dos outros projetos, o arquivo zip acima:
![]() | ![]() ![]() | ![]() |
No Android Studio, abra o projeto Gradle [server-01] [1-2]. O projeto aberto é o [3] (perspectiva Projeto).
1.16.1.2. Configuração do Gradle
![]() |
O arquivo Gradle gerado (junho de 2016) é o seguinte:
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
eclipse {
classpath {
containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
}
}
- as linhas 14 e 34-38 são para o Eclipse IDE. Nós as excluímos;
- as linhas 1 a 11 e 15 servem para adicionar um plugin chamado [spring-boot] ao nosso projeto Gradle. O Spring Boot é um projeto do ecossistema Spring [http://projects.spring.io/spring-boot/]. Esse plugin define as versões das dependências mais comumente utilizadas com o Spring. Isso permite que não seja necessário especificar suas versões (linhas 30 e 31). A versão é, então, aquela definida pela versão do Spring Boot utilizada (linha 3);
- linhas 22-23: a versão do Java a ser utilizada, neste caso a versão 1.8;
- linhas 25-27: os repositórios de binários a serem usados para baixar as dependências;
- linha 26: indica o repositório central do Maven. Atualmente, é o maior repositório de binários de código aberto disponível;
- linhas 29-32: as dependências necessárias para o projeto:
- linha 30: essa dependência traz consigo todos os binários necessários para construir um serviço web Spring;
- linha 31: essa dependência traz consigo todos os binários necessários para os testes, especialmente para os testes JUnit;
- uma dependência [compile] indica que precisamos dela para compilar o projeto. Uma dependência [testCompile] indica que precisamos dela apenas para a execução dos testes. Portanto, ela não é incluída no binário do projeto;
Fazemos uma primeira limpeza do arquivo Gradle:
// Spring Boot
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
// plug-ins
apply plugin: 'java'
apply plugin: 'spring-boot'
// binário do projeto
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
// versões do Java
sourceCompatibility = 1.8
targetCompatibility = 1.8
// repositórios Maven
repositories {
mavenLocal()
mavenCentral()
}
// dependências
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
- linha 30: adicionamos o repositório Maven local da estação de desenvolvimento. Ele é criado quando se instala o Maven (ver parágrafo 6.10). Se a dependência solicitada já estiver no repositório Maven local, ela não será solicitada ao repositório Maven central;
- linhas 19-22: uma tarefa do Gradle que permite gerar o binário do projeto. Vamos utilizá-la para ver o que é feito;
![]() | ![]() | ![]() |
- em [1-4], execute a tarefa [jar] definida no arquivo [build.gradle] ([1] está localizado no canto superior direito e ao lado do IDE);
A operação anterior cria o arquivo jar do projeto e o coloca na pasta [build / libs] [5]:
![]() |
O nome do arquivo provém diretamente das informações fornecidas à tarefa [jar] no arquivo [build.gradle] (linhas 19-22).
Todas as dependências do projeto podem ser visualizadas da seguinte forma:
![]() |
É possível observar no [1] que a única dependência do projeto [compile('org.springframework.boot:spring-boot-starter-web')] trouxe consigo dezenas de binários. O Spring Boot para web incluiu as dependências das quais uma aplicação web Spring MVC provavelmente precisará. Isso significa que algumas delas podem ser desnecessárias. O Spring Boot é ideal para um tutorial:
- ele traz as dependências das quais provavelmente precisaremos;
- ele traz um servidor Tomcat integrado [1], o que nos poupa de ter que implantar a aplicação em um servidor web externo;
É possível encontrar diversos exemplos que utilizam o Spring Boot no site do ecossistema Spring [http://spring.io/guides].
Agora, preenchemos o arquivo [build.gradle] da seguinte maneira:
// Spring Boot
...
// dependências
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
// plug-in para criar um binário de acordo com as normas do Maven no repositório local do Maven
apply plugin: 'maven-publish'
publishing {
publications {
maven(MavenPublication) {
groupId 'istia.st.exemples.android'
artifactId 'server-01'
version '0.0.1-SNAPSHOT'
from components.java
}
}
repositories {
maven {
// Altere para apontar para o seu repositório, e.g. http://my.org/repo
url 'file://D:\\maven'
}
}
}
- linha 10: importamos um plugin do Gradle chamado [maven-publish], que permite publicar o binário do projeto em um repositório Maven, respeitando as normas do Maven;
- linha 11: uma tarefa do Gradle chamada [publishing];
- linhas 14-15: as características do binário Maven que será criado;
- linha 23: o repositório Maven no qual ele será publicado, neste caso, um repositório Maven local;
A adição do plugin [maven-publish] criou novas tarefas no projeto Gradle:
![]() | ![]() |
Se, no [2], executarmos a tarefa [publish], o binário do projeto será criado e instalado na pasta indicada na linha 23 do arquivo [build.gradle]:
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |
![]() |
A tarefa [jar] permite gerar o binário do projeto. Esse binário não contém suas dependências e, portanto, não é executável. É possível gerar um binário com todas as suas dependências e que seja executável. Para isso, adicionamos ao arquivo [build.gradle] o seguinte código:
// criar um binário com todas as suas dependências
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'istia.st.exemples.android.Server01Application'
}
baseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
- linha 6: é necessário inserir o nome completo da classe executável do projeto:
![]() |
O código dessa classe será o seguinte:
package istia.st.exemples.android;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Server01Application {
public static void main(String[] args) {
System.out.println("Server01Application running");
//SpringApplication.run(Server01Application.class, args);
}
}
Atualize o projeto Gradle e, em seguida, execute a tarefa [fatJar]:
![]() | ![]() |
O binário é gerado na pasta [build / libs] e pode ser executado como [1-7]:
![]() | ![]() |
1.16.1.3. Configuração do projeto
A configuração do Gradle não é suficiente. Também precisamos configurar o projeto. Como não se trata de um projeto Android gerado pelo IDE, essa configuração, que não havíamos feito até agora, deve ser realizada neste momento.
![]() | ![]() |
- no [3-4]: use um JDK 1.8;
Para compilar o projeto, o botão disponível para projetos Android não está mais presente. Usaremos uma opção do menu [1-2]:
![]() | ![]() |
A seguir, o leitor é convidado a criar o projeto a seguir. Comentamos o código final do projeto [3].
1.16.1.4. A camada [métier]
![]() |
![]() |
A camada [métier] segue o mesmo princípio da camada [métier] do exemplo anterior. Ela terá a seguinte interface [IMetier]:
package exemples.android.server.metier;
public interface IMetier {
// número aleatório em [a,b]
int getAlea(int a, int b);
}
- linha 5: o método que gera um número aleatório em [a,b]
O código da classe [Metier] que implementa essa interface é o seguinte:
package exemples.android.server.metier;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Random;
@Service
public class Metier implements IMetier {
@Override
public int getAlea(int a, int b) {
// algumas verificações
if (a < 0) {
throw new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0", 2);
}
if (b < 0) {
throw new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0", 3);
}
if (a >= b) {
throw new AleaException("Dans l'intervalle [a,b], on doit avoir a< b", 4);
}
// geração do resultado
Random random=new Random();
random.setSeed(new Date().getTime());
return a + random.nextInt(b - a + 1);
}
}
Não comentaremos a classe: ela é análoga à encontrada no exemplo anterior, exceto pelo fato de não lançar exceções aleatoriamente. Observe-se apenas, na linha 8, a anotação Spring [@Service], que fará com que o Spring instancie a classe em uma única instância (singleton) e disponibilize sua referência para outros componentes do Spring. Outras anotações do Spring poderiam ter sido utilizadas aqui para o mesmo efeito. Os componentes do Spring possuem nomes padrão que podem ser especificados como atributo da anotação utilizada. Sem esse atributo, como neste caso, o componente do Spring recebe o nome da classe com o primeiro caractere em minúscula. Assim, neste caso, o componente do Spring recebe, por padrão, o nome [metier];
A classe [Metier] lança exceções do tipo [AleaException]:
package exemples.android.server.metier;
public class AleaException extends RuntimeException {
// código de erro
private int code;
// construtores
public AleaException() {
}
public AleaException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public AleaException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
public AleaException(String detailMessage, Throwable throwable, int code) {
super(detailMessage, throwable);
this.code = code;
}
// getters e setters
....
}
- linha 3: [AleaException] estende a classe [RuntimeException]. Trata-se, portanto, de uma exceção não controlada (não é obrigatório tratá-la com um try/catch);
- linha 6: adiciona-se um código de erro à classe [RuntimeException];
1.16.1.5. O serviço web / jSON
![]() |
![]() |
O serviço web / jSON é implementado pelo Spring MVC. O Spring MVC implementa o modelo de arquitetura denominado MVC (Modelo – Visão – Controlador) da seguinte maneira:
![]() |
O processamento de uma solicitação de um cliente ocorre da seguinte maneira:
- solicitação — as URL solicitadas têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... A [Dispatcher Servlet] é a classe do Spring que processa as URL recebidas. Ela “encaminha” o URL para a ação que deve processá-lo. Essas ações são métodos de classes específicas chamadas [Contrôleurs]. O C de MVC é, neste caso, a cadeia [Dispatcher Servlet, Contrôleur, Action]. Se nenhuma ação tiver sido configurada para processar o URL recebido, o servlet [Dispatcher Servlet] responderá que o URL solicitado não foi encontrado (erro 404 NOT FOUND);
- processamento
- a ação selecionada pode utilizar os parâmetros parami que o servlet [Dispatcher Servlet] lhe transmitiu. Esses parâmetros podem provir de várias fontes:
- do caminho [/param1/param2/...] do URL,
- dos parâmetros [p1=v1&p2=v2] do URL,
- dos parâmetros enviados pelo navegador junto com sua solicitação;
- No processamento da solicitação do usuário, a ação pode precisar da camada [metier] [2b]. Uma vez processada a solicitação do cliente, ela pode gerar diversas respostas. Um exemplo clássico é:
- uma página de erro, caso a solicitação não tenha sido processada corretamente
- uma página de confirmação, caso contrário
- a ação solicita que uma determinada visualização seja exibida: [3]. Essa visualização exibirá dados conhecidos como modelo da visualização. Esse é o M de MVC. A ação criará esse modelo M [2c] e solicitará que uma vista V seja exibida [3];
- resposta — a vista V selecionada utiliza o modelo M criado pela ação para inicializar as partes dinâmicas da resposta HTML que ela deve enviar ao cliente e, em seguida, envia essa resposta.
Para um serviço web / jSON, a arquitetura anterior é ligeiramente modificada:
![]() |
- em [4a], o modelo, que é uma classe Java, é transformado na string jSON por uma biblioteca jSON;
- em [4b], essa string jSON é enviada ao navegador;
Um exemplo de serialização de um objeto Java na string jSON e de desserialização de uma string jSON em um objeto Java é apresentado nos anexos do parágrafo 6.14.
Voltemos à camada [web] de nossa aplicação:
![]() |
Em nossa aplicação, há apenas um controlador:
![]() |
O serviço web / jSON enviará aos seus clientes uma resposta do tipo [Response] da seguinte forma:
package exemples.android.server.web;
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
...
}
- linha 13: o campo [T body] é a resposta esperada pelo cliente. Decidimos usar aqui uma resposta genérica do tipo T, em vez do tipo Integer, que corresponde ao número aleatório esperado. Queremos poder reutilizar essa classe em outras situações. Durante o processamento da solicitação do cliente, o servidor pode encontrar um problema, que é então resumido nos outros dois campos;
- linha 8: um código de status (0 se não houver erro);
- linha 9: se status!=0, uma lista de mensagens de erro, geralmente as da pilha de exceções caso tenha ocorrido uma exceção; null se não houver erros;
O controlador [WebController] é o seguinte:
package exemples.android.server.web;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.server.metier.AleaException;
import exemples.android.server.metier.IMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.List;
@Controller
public class WebController {
// camada de negócios
@Autowired
private IMetier metier;
// mapper JSON
@Autowired
private ObjectMapper mapper;
// números aleatórios
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
// a resposta
Response<Integer> response = new Response<>();
// utiliza-se a camada de negócios
try {
response.setBody(metier.getAlea(a, b));
response.setStatus(0);
} catch (AleaException e) {
response.setStatus(e.getCode());
response.setMessages(getMessagesFromException(e));
}
// retornamos a resposta
return mapper.writeValueAsString(response);
}
private List<String> getMessagesFromException(Throwable e) {
// lista de mensagens
List<String> messages = new ArrayList<String>();
// percorre-se a pilha de exceções
Throwable th = e;
while (th != null) {
messages.add(e.getMessage());
th = th.getCause();
}
// retornamos o resultado
return messages;
}
}
- linha 17: a anotação [@Controller] indica que a classe é um controlador MVC cujos métodos processam solicitações para determinadas URL da aplicação web;
- linhas 21-22: a anotação [@Autowired] solicita que o Spring injete no campo um componente do tipo [IMetier]. Essa será a classe [Metier] mencionada anteriormente. É porque colocamos nessa classe a anotação [@Service] que ela é tratada como um componente do Spring;
- linhas 24-25: fazemos o mesmo com um mapeador jSON, que definiremos posteriormente. Nosso serviço web enviará sua resposta na forma de uma string jSON. É esse mapeador que realizará a serialização da resposta em jSON;
- linha 30: o método que gera o número aleatório. Seu nome não importa. Quando ele é executado, seus parâmetros já foram inicializados pelo Spring MVC. Veremos como. Além disso, se ele for executado, é porque o servidor web recebeu uma solicitação HTTP GET para o URL da linha 28;
- linha 28: a anotação [@RequestMapping] define certas propriedades do método anotado:
- [value]: o URL aceito pelo método;
- [method]: o método HTTP aceito pelo método. Existem basicamente duas, GET e POST. O método [POST] é utilizado quando o cliente deseja anexar um documento à sua solicitação HTTP;
- [produces]: define um dos cabeçalhos da resposta HTTP que será enviada ao cliente. Nesse caso, entre os cabeçalhos HTTP enviados com a resposta ao cliente, haverá um que informará a ele que a resposta está sendo enviada na forma de uma sequência jSON. Esse cabeçalho não é obrigatório. Ele é fornecido a título informativo ao cliente caso este esteja aguardando respostas que possam assumir diversas formas;
- [consumes]: não está presente aqui. Ela permite indicar os cabeçalhos HTTP que devem acompanhar a solicitação HTTP do cliente para que ela seja aceita;
- linha 29: a anotação [@ResponseBody] indica que o resultado gerado pelo método deve ser enviado ao cliente. Sem essa anotação, a resposta do método é considerada uma chave que permite selecionar a página HTML a ser enviada ao cliente. Em um serviço web / jSON, não há páginas HTML;
- linha 28: a URL processada tem o formato /{a}/{b}, em que {x} representa uma variável. As variáveis {a} e {b} são atribuídas aos parâmetros do método na linha 30. Isso é feito por meio da anotação @PathVariable("x"). Observe-se que {a} e {b} são componentes de um URL e, portanto, são do tipo String. A conversão de String para o tipo dos parâmetros pode falhar. O Spring MVC, então, lança uma exceção. Resumindo: se, usando um navegador, eu solicitar o URL /100/200, o método getAlea da linha 30 será executado com os parâmetros inteiros a=100, b=200;
- linha 36: solicita-se à camada [métier] um número aleatório no intervalo [a,b]. Lembramos que o método [metier].getAlea pode lançar uma exceção;
- linha 37: sem erros;
- linha 39: código de erro;
- linha 40: a lista de mensagens da resposta é a da pilha de exceções (linhas 46-57). Aqui, sabemos que a pilha contém apenas uma exceção, mas quisemos mostrar um método mais genérico;
- linha 43: a resposta do tipo [Response<Integer>] é retornada na forma de uma string jSON;
1.16.1.6. Configuração do projeto Spring
![]() |
Existem várias maneiras de configurar o Spring:
- com os arquivos XML;
- com código Java;
- com uma combinação dos dois;
Optamos por configurar nossa aplicação web com código Java. É a seguinte classe [Config] que garante essa configuração:
package exemples.android.server.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@ComponentScan(basePackages = { "exemples.android.server.metier", "exemples.android.server.web" })
@EnableWebMvc
public class Config {
// configuração da web ------------------------------------
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// mapeador jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- linha 12: informamos ao Spring em quais pacotes ele encontrará os dois componentes que deve gerenciar:
- o componente [Metier], anotado com [@Service], no pacote [exemples.android.server.metier];
- o componente [WebController], anotado como [@Controller], no pacote [exemples.android.server.web];
- linha 13: a anotação [@EnableWebMvc] permite que o Spring Boot realize por conta própria uma série de configurações padrão para uma aplicação Spring MVC. Isso alivia o trabalho do desenvolvedor;
- linhas 16, 22, 27 e 33: a anotação [@Bean] também define componentes (beans) do Spring, da mesma forma que as duas anotações mencionadas anteriormente (@Service, @Controller). Aqui, a anotação [@Bean] anota um método e não uma classe, e é o resultado do método que constitui o componente Spring. Na ausência de um atributo de nomenclatura na anotação [@Bean], o componente Spring criado recebe o nome do método anotado;
- linhas 16-20: definem o bean [dispatcherServlet]. Trata-se de um nome predefinido do Spring, MVC, que define o front controller da aplicação MVC, um objeto pelo qual passam todas as solicitações dos clientes e que as distribui (daí seu nome) para os diferentes [@Controller] da aplicação Spring MVC;
- linha 18: o bean [dispatcherServlet] é uma instância da classe [DispatcherServlet] fornecida pelo Spring MVC;
- linhas 22-25: o bean [servletRegistrationBean] serve para definir quais URL são aceitos pela aplicação. Na linha 24, aceitam-se todos os URL;
- linhas 27-30: o bean [embeddedServletContainerFactory] serve para definir o servidor incorporado nas dependências do projeto que deve hospedar a aplicação web. A linha 29 indica que se trata de um servidor Tomcat e que ele funcionará na porta 8080. Por padrão, os binários desse servidor web são fornecidos pela dependência [org.springframework.boot:spring-boot-starter-web] do arquivo Gradle;
1.16.1.7. Execução do serviço web / jSON
![]() |
O projeto é executado a partir da seguinte classe executável [Boot]:
package exemples.android.server.boot;
import exemples.android.server.config.Config;
import org.springframework.boot.SpringApplication;
public class Boot {
public static void main(String[] args) {
// execução do aplicativo
SpringApplication.run(Config.class, args);
}
}
- a classe [Boot] é uma classe executável (linhas 7-10);
- linha 9: o método estático [SpringApplication.run] é um método de [spring Boot] (linha 4) que iniciará o aplicativo. Seu primeiro parâmetro é a classe Java que configura o projeto. Nesse caso, a classe [Config] que acabamos de descrever. O segundo parâmetro é o array de argumentos passado ao método [main] (linha 7);
É possível iniciar o aplicativo web de várias maneiras, incluindo a seguinte:
![]() |
Na console, aparecem então vários registros:
- linhas 12-14: o servidor Tomcat integrado é iniciado;
- linhas 15-19: o servlet [DispatcherServlet] do Spring MVC é carregado e configurado;
- linha 20: a URL [/{a}/{b}] do servidor web é detectada;
Agora, vamos abrir um navegador e testar o URL do serviço web / jSON:
![]() |
![]() |
![]() |
![]() |
Em cada caso, obtemos a representação jSON de um objeto do tipo [Response<Integer>].
Em vez de usar um navegador padrão, vamos agora utilizar a extensão [Advanced Rest Client] do navegador Chrome (ver anexos, parágrafo 6.13):

- em [1], o URL solicitado;
- em [2], por meio de um GET;
- em [3], a solicitação é enviada;

- em [4], os cabeçalhos HTTP da resposta do servidor. Observe-se que este indica que o documento enviado é uma sequência jSON;
- em [5], a sequência jSON recebida;
1.16.1.8. Geração do arquivo jar executável do projeto
No parágrafo 1.16.1.2, mostramos como configurar o arquivo Gradle para gerar um executável do aplicativo com todas as suas dependências. Adaptada ao aplicativo em questão, essa configuração fica da seguinte forma:
// criar um binário com todas as suas dependências
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'exemples.android.server.boot.Boot'
}
baseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
Para gerar esse executável, pode-se proceder da seguinte forma [1-5]:
![]() | ![]() |
Para executá-lo, interrompa o serviço web, caso esteja em execução ([1]), e, em seguida, execute o arquivo ([2-4]):
![]() | ![]() |
Abra um navegador e acesse o URL e o [localhost:8080/100/200]. Você deve obter os mesmos resultados de antes.
1.16.1.9. Gerenciamento de logs
Ao executar o arquivo executável, percebe-se que os logs não são os mesmos que os gerados ao executar o projeto a partir do IDE. Os logs são gerados no modo [DEBUG]:
...
09:32:03.741 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletConfigInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletContextInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemProperties]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemEnvironment]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source. Returning [null]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: Using a shared selector for servlet write/read
09:32:03.810 [main] INFO org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:32:03.813 [main] INFO exemples.android.server.boot.Boot - Started Boot in 1.984 seconds (JVM running for 2.206)
É possível gerenciar o nível dos logs adicionando um arquivo [logback.xml] na pasta [resources] do projeto:
![]() |
Esse arquivo poderia ter o seguinte conteúdo:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!--é atribuído, por padrão, o tipo
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- controle de nível dos logs -->
<root level="info"> <!-- info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
O nível dos logs é controlado na linha 12. Se, agora, regenerarmos o arquivo executável e o executarmos, teremos apenas logs de nível [info]:
...
09:36:52.433 [main] INFO o.h.validator.internal.util.Version - HV000001: Hibernate Validator 5.2.4.Final
09:36:52.762 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Tue Jun 07 09:36:51 CEST 2016]; root of context hierarchy
09:36:52.811 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/{a}/{b}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String exemples.android.server.web.WebController.getAlea(int,int) throws com.fasterxml.jackson.core.JsonProcessingException
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: Using a shared selector for servlet write/read
09:36:52.923 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:36:52.926 [main] INFO exemples.android.server.boot.Boot - Started Boot in 1.865 seconds (JVM running for 2.203)
1.16.2. O cliente Android do servidor web / jSON
O cliente Android terá a seguinte arquitetura:
![]() |
O cliente terá dois componentes:
- uma camada [Présentation] (visualização + atividade) semelhante à que estudamos no exemplo [Exemple-14];
- a camada [DAO], que se comunica com o serviço [web / jSON] que estudamos anteriormente.
1.16.2.1. Criação do projeto
Duplicamos o projeto anterior [Exemple-14] em [Exemple-15], seguindo o procedimento descrito no parágrafo 1.4. Obtemos o seguinte resultado:
![]() | ![]() |
A seguir, o leitor é convidado a criar o projeto a seguir.
1.16.2.2. Configuração do Gradle
![]() |
O arquivo [build.gradle] é o seguinte:
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 {
applicationId "exemples.android"
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 fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
Comentaremos apenas o que ainda não foi abordado:
- linhas 46-47: inserção de um plugin AA. O plugin [rest-spring-api] permite delegar à biblioteca AA as comunicações cliente/servidor;
- linha 50: a biblioteca [spring-android-rest-template] é a biblioteca utilizada pelo AA para garantir as trocas cliente/servidor. A versão [2.0.0.M3] é uma versão denominada “milestone”, que não se encontra nos repositórios habituais do Maven. Portanto, é necessário especificar, nas linhas 56 a 59, o repositório a ser utilizado (linha 58) para localizar a biblioteca;
- linha 51: uma biblioteca jSON;
- linhas 33 a 39: sem essa propriedade, ocorrem erros no momento da geração do binário APK do projeto;
1.16.2.3. O manifesto do aplicativo Android
![]() |
O arquivo [AndroidManifest.xml] precisa ser alterado. De fato, por padrão, os acessos à Internet estão desativados. É preciso ativá-los por meio de uma diretiva especial:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.MainActivity_"
android:label="@string/app_name"
android:windowSoftInputMode="stateHidden"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- linha 5: os acessos à Internet estão autorizados;
1.16.2.4. A camada [DAO]
![]() |
![]() |
1.16.2.4.1. A interface [IDao] da camada [DAO]
A interface da camada [DAO] será a seguinte:
package exemples.android.dao;
public interface IDao {
// número aleatório
int getAlea(int a, int b);
// URL do serviço web
void setUrlServiceWebJson(String url);
// tempo máximo de espera (ms) pela resposta do servidor
void setTimeout(int timeout);
// tempo de espera do cliente, em milissegundos, antes da solicitação
void setDelay(int delay);
}
- linha 6: o método do serviço web / jSON para obter um número aleatório no intervalo [a,b] desse serviço web;
- linha 9: o URL do serviço web / jSON de geração de números aleatórios;
- linha 12: define-se um tempo máximo de espera pela resposta do servidor;
- linha 15: define-se um tempo de espera antes da execução da solicitação ao servidor, a fim de dar tempo ao usuário para cancelar sua solicitação;
1.16.2.4.2. A interface [WebClient]
![]() |
A interface [WebClient] é responsável pela comunicação com o serviço web. Seu código é o seguinte:
package exemples.android.dao;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// 1 número aleatório no intervalo [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- linha 12: [WebClient] é uma interface que a biblioteca AA irá implementar por conta própria, graças às anotações que serão inseridas nela. Essa interface deve implementar as chamadas para URL expostas pelo serviço web / jSON:
// número aleatório
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
- linha 11: a anotação [@Rest] é uma anotação AA. O valor do atributo [converters] é uma matriz de conversores. Aqui, o conversor [MappingJackson2HttpMessageConverter.class] faz com que, quando o servidor envia uma string jSON, esta seja automaticamente deserializada. Assim, vemos na linha (d) que o URL [/{a}/{b}] retorna um tipo String, que na verdade é uma string jSON (linha b). Com essas informações e as do tipo esperado na linha 16, a instância [WebClient] do cliente irá deserializar a cadeia que receberá em um tipo [Response<Integer>];
- linha 15: uma anotação AA indicando que o URL deve ser chamado com um método HTTP GET. O parâmetro da anotação [@Get] é o formato do URL esperado pelo serviço web. Basta retomar o parâmetro [value] da anotação [@RequestMapping] (linha b) do método chamado no controlador [WebController] do servidor. As chaves {} envolvem os parâmetros do URL que devem ser retomados nos parâmetros do método na linha 16. A sintaxe [@Path("a") int a] faz com que o parâmetro [a] do método seja atribuído ao valor {a} do URL. Quando o parâmetro de URL e o do método têm o mesmo nome, como neste caso, pode-se escrever de forma mais simples [@Path int a];
No caso de uma consulta HTTP POST, o método de chamada teria a seguinte assinatura:
@Post("/{a}/{b}")
Response<Integer> getAlea(@Body T body, @Path("a") int a, @Path("b") int b);
É a anotação [@Body] que designa o valor enviado. Este será automaticamente serializado como jSON. No lado do servidor, teremos a seguinte assinatura:
// números aleatórios
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b, @RequestBody T body) {
- linha 2: especifica-se que se espera uma solicitação HTTP POST e que o corpo dessa solicitação (objeto enviado) deve ser transmitido na forma de uma string jSON (atributo consumes);
- linha 4: o valor enviado será recuperado no parâmetro [@RequestBody T body] do método;
Voltemos ao código da classe [WebClient]:
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
- precisamos poder indicar o URL do serviço web a ser contatado. Isso é obtido estendendo a interface [RestClientRootUrl] fornecida por AA. Essa interface expõe um método [setRootUrl(urlServiceWeb] que permite definir o URL do serviço web a ser contatado;
- além disso, queremos controlar a chamada ao serviço web, pois desejamos limitar o tempo de espera pela resposta. Para isso, estendemos a interface [RestClientSupport], que expõe o método [setRestTemplate], o qual nos permitirá:
- criar nós mesmos o objeto [RestTemplate], que serve para gerenciar as trocas entre cliente e servidor;
- configurar esse objeto para definir o tempo máximo de espera pela resposta;
1.16.2.4.3. A classe [Response]
O método [getAlea] da interface [IDao] retorna uma resposta do tipo [Response] da seguinte forma:
package exemples.android.dao;
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
...
}
Trata-se da classe [Response] já utilizada no lado do servidor (parágrafo 1.16.1.5). Na verdade, do ponto de vista da programação, tudo ocorre como se a camada [DAO] do cliente se comunicasse diretamente com o controlador [WebController] do serviço web:
![]() |
A comunicação de rede entre cliente e servidor, bem como a serialização/desserialização dos objetos Java no lado do cliente, são transparentes para o programador.
1.16.2.4.4. Implementação da camada [DAO]
![]() |
A interface [IDao] é implementada com a seguinte classe [Dao]:
package exemples.android.dao;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
@EBean
public class Dao implements IDao {
// cliente do serviço REST
@RestService
protected WebClient webClient;
// mapeador jSON
private ObjectMapper mapper = new ObjectMapper();
// tempo de espera antes da execução da solicitação
private int delay;
// interface IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
...
}
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
...
}
@Override
public void setTimeout(int timeout) {
...
}
@Override
public void setDelay(int delay) {
this.delay = delay;
}
}
- linha 15: anotamos a classe [Dao] com a anotação [@EBean] para transformá-la em um bean AA que poderemos injetar em outro lugar;
- linhas 19-20: injetamos a implementação que será feita da interface [WebClient] que descrevemos. É a anotação [@RestService] que garante essa injeção;
- os demais métodos implementam a interface [IDao] (linhas 27-46);
Método [setTimeout]
O método [setTimeout] é o seguinte:
@Override
public void setTimeout(int timeout) {
// define-se o tempo limite para as solicitações do cliente REST
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
// construindo o restTemplate
RestTemplate restTemplate = new RestTemplate(factory);
// define-se o conversor jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// define-se o restTemplate do cliente web
webClient.setRestTemplate(restTemplate);
}
- a interface [WebClient] será implementada por uma classe AA utilizando a dependência Gradle [org.springframework.android:spring-android-rest-template]. [spring-android-rest-template] implementa a comunicação do cliente com o servidor web / jSON por meio de uma classe do tipo [RestTemplate];
- linha 4: a classe [SimpleClientHttpRequestFactory] é fornecida pela dependência [spring-android-rest-template]. Ela nos permitirá definir o tempo máximo de espera pela resposta do servidor (linhas 5-6);
- linha 8: criamos o objeto do tipo [RestTemplate], que servirá de suporte para a comunicação com o serviço web. Passamos a ele, como parâmetro, o objeto [factory] que acabou de ser criado;
- linha 10: o diálogo cliente/servidor pode assumir diversas formas. As trocas ocorrem por meio de linhas de texto e precisamos indicar ao objeto do tipo [RestTemplate] o que ele deve fazer com essa linha de texto. Para isso, fornecemos a ele conversores, ou seja, classes capazes de processar as linhas de texto. A escolha do conversor geralmente é feita por meio dos cabeçalhos HTTP que acompanham a linha de texto. Neste caso, sabemos que recebemos apenas linhas de texto no formato jSON. Além disso, vimos no parágrafo 1.16.1.7 que o servidor enviava o cabeçalho HTTP:
Content-Type: application/json;charset=UTF-8
Na linha 10, o único conversor do [RestTemplate] será um conversor jSON implementado com a biblioteca [Jackson]. Há uma peculiaridade em relação a esses conversores: o AA exige que ele também conste na anotação do cliente web [WebClient]:
@Rest(converters = {MappingJacksonHttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
Na linha 1, somos obrigados a especificar um conversor, mesmo que já o tenhamos especificado por meio de programação.
- Linha 12: o objeto [RestTemplate] assim construído é injetado na implementação da interface [WebClient], e é esse objeto que irá operar o diálogo cliente/servidor;
Método [getAlea]
O método [getAlea] é o seguinte:
@Override
public int getAlea(int a, int b) {
// execução do serviço
Response<Integer> info;
DaoException ex;
try {
// em espera
waitSomeTime(delay);
// execução do serviço
info = webClient.getAlea(a, b);
int status = info.getStatus();
if (status == 0) {
// retornando o resultado
return info.getBody();
} else {
// registra-se a exceção
ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
}
} catch (JsonProcessingException | RuntimeException e) {
// registra-se a exceção
ex = new DaoException(e, 100);
}
// lançamento da exceção
throw ex;
}
...
// métodos privados -------------------
private void waitSomeTime(int delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- linha 8: aguarda [delay] milissegundos;
- linha 10: basta chamar o método com a mesma assinatura na classe que implementa a interface [WebClient];
- linha 11: analisa-se a resposta obtida do servidor verificando seu [status];
- linhas 12-14: se não houve erro no servidor (status=0), retorna-se o resultado do método;
- linha 17: se houve erro no servidor (status!=0), prepara-se uma exceção sem lançá-la. O servidor transmitiu uma lista de mensagens de erro. Criamos uma exceção com, como única mensagem, a string jSON da lista de mensagens do servidor;
- linhas 19-22: outros casos de exceção;
- linha 24: quando chegamos aqui, necessariamente ocorreu uma exceção. Então, a lançamos;
A exceção [DaoException] utilizada por este código é a seguinte:
package exemples.android.dao;
import java.util.ArrayList;
import java.util.List;
public class DaoException extends RuntimeException {
// código de erro
private int code;
// construtores
public DaoException() {
}
public DaoException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public DaoException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
// getters e setters
...
}
- linha 6: a exceção [DaoException] é uma exceção não controlada;
Método [setUrlServiceWebJson]
O método [setUrlServiceWebJson] é o seguinte:
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// define-se o URL do serviço REST
webClient.setRootUrl(urlServiceWebJson);
}
- linha 4: define-se o URL do serviço web por meio do método [setRootUrl] da interface [WebClient]. É porque essa interface estende a interface [RestClientRootUrl] que esse método existe;
1.16.2.5. O pacote [architecture]
O pacote [architecture] reúne os elementos que estruturam a aplicação:
![]() |
![]() |
1.16.2.5.1. A interface [IMainActivity]
A interface [IMainActivity] lista os métodos que a atividade do aplicativo deve implementar:
package exemples.android.architecture;
import exemples.android.dao.IDao;
public interface IMainActivity extends IDao {
// acesso à sessão
Session getSession();
// mudança de visualização
void navigateToView(int position);
// espera
void beginWaiting();
void cancelWaiting();
// modo de depuração
boolean IS_DEBUG_ENABLED = true;
// tempo de espera pela resposta
int TIMEOUT = 1000;
// adjacência dos fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
}
- linha 5: a interface [IMainActivity] estende a interface [IDao];
- linhas 13-16: aos métodos já presentes nos exemplos anteriores (linhas 7-11), adicionamos dois métodos para gerenciar a imagem de espera do aplicativo (linhas 14, 16);
- linha 21: definimos um tempo máximo de espera pela resposta do servidor em 1 segundo;
1.16.2.5.2. A classe [Utils]
Reunimos na classe [Utils] métodos utilitários estáticos que podem ser chamados de diferentes pontos da arquitetura do aplicativo:
package exemples.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 getMessagesForAlert(Throwable th) {
// construindo o 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();
}
}
- linhas 9-18: cria a lista de mensagens de erro contidas em um Throwable;
- linhas 21-32: utiliza o método anterior para construir, a partir da lista de mensagens obtida, o texto a ser exibido em uma mensagem de alerta do Android;
- linhas 27-28: as mensagens são numeradas. O número menor (1) corresponde à exceção inicial e o número maior, à exceção mais recente na pilha de exceções;
1.16.2.5.3. A classe abstrata [AbstractFragment]
A classe [AbstractFragment] tem duas funções:
- garantir que o método [updateFragments] das classes filhas seja sempre chamado ao exibir o fragmento, e apenas uma vez;
- fatorar o estado e os métodos das classes filhas que possam ser fatorados;
É a função 2 que nos leva a incluir nesta classe as operações de gerenciamento da imagem de espera: todos os fragmentos de um aplicativo Android assíncrono precisam lidar com esse tipo de problema:
// gerenciamento da espera
protected void beginWaiting() {
// exibição da ampulheta
mainActivity.beginWaiting();
}
protected void cancelWaiting() {
// retirando a ampulheta
mainActivity.cancelWaiting();
}
1.16.2.6. A visualização
![]() |
1.16.2.6.1. A visualização [vue1.xml]
![]() |
Em comparação com o exemplo anterior, a visualização [vue1.xml] sofre as seguintes alterações:
![]() |
![]() |
- em [1], o usuário deve especificar o URL do serviço web, bem como o tempo de espera [2] antes de cada chamada ao serviço web;
- em [3], as respostas são contadas;
- em [4], o usuário pode cancelar sua solicitação;
- em [5], um indicador de espera é exibido quando os números são solicitados. Ele desaparece quando todos os números forem recebidos ou quando a operação for cancelada;

- em [6], a validade das entradas é verificada;
O usuário é solicitado a carregar o arquivo [vue1.xml] a partir dos exemplos. A seguir, fornecemos os identificadores dos novos componentes:

Os botões [10-11] estão fisicamente sobrepostos. Em determinado momento, apenas um deles ficará visível.
1.16.2.6.2. O fragmento [Vue1Fragment]
![]() |
A estrutura do fragmento [Vue1Fragment] é a seguinte:
package exemples.android.fragments;
import android.app.AlertDialog;
import android.support.annotation.*;
import android.support.v4.app.Fragment;
import android.view.View;
import android.widget.*;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.*;
import org.androidannotations.annotations.UiThread;
import org.androidannotations.api.BackgroundExecutor;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// elementos da interface visual
@ViewById(R.id.editTextUrlServiceWeb)
EditText edtUrlServiceRest;
@ViewById(R.id.textViewErreurUrl)
TextView txtMsgErreurUrlServiceWeb;
@ViewById(R.id.editTextDelay)
EditText edtDelay;
@ViewById(R.id.textViewErreurDelay)
TextView textViewErreurDelay;
@ViewById(R.id.lst_reponses)
ListView listReponses;
@ViewById(R.id.txt_Reponses)
TextView infoReponses;
@ViewById(R.id.edt_nbaleas)
EditText edtNbAleas;
@ViewById(R.id.edt_a)
EditText edtA;
@ViewById(R.id.edt_b)
EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
TextView txtErrorIntervalle;
@ViewById(R.id.btn_Executer)
Button btnExecuter;
@ViewById(R.id.btn_Annuler)
Button btnAnnuler;
...
// dados locais
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
@AfterViews
void afterViews() {
// memória
afterViewsDone=true;
// inicialmente, sem mensagens de erro
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// botão [Annuler] oculto
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
// lista de respostas
reponses = new ArrayList<>();
}
...
- linhas 24-49: as referências aos componentes da visualização [vue1.xml] (linha 20);
- linhas 55-69: o método [@AfterViews] executado quando as referências das linhas 24-49 foram inicializadas;
- linha 58: não se esqueça — necessário para o ciclo de vida do fragmento;
- linhas 60-63: as mensagens de erro são ocultadas;
- linhas 65-66: oculta-se o botão [Annuler] (linha 65) e exibe-se o botão [Exécuter] (linha 66). Vale lembrar que eles estão fisicamente um sobre o outro;
- linha 68: o campo da linha 52 conterá a lista de cadeias de caracteres a serem exibidas pelo ListView das respostas;
Logo após o método [@AfterViews], o método [updateFragment] a seguir será executado:
@Override
protected void updateFragment() {
// criando o adaptador da lista de respostas
adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
- linhas 4-5: cria-se o adaptador ListView das respostas. Ele é armazenado em uma variável de instância para ficar disponível para os outros métodos da classe;
Ao clicar no botão [Exécuter], o seguinte método é executado:
// as entradas
private int nbAleas;
private int a;
private int b;
private String urlServiceWebJson;
private int delay;
// dados locais
private int nbInfos;
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
private boolean hasBeenCanceled;
@Click(R.id.btn_Executer)
protected void doExecuter() {
// apaga-se as respostas anteriores
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// zeramos o contador de respostas
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// verifica-se a validade das entradas
if (!isPageValid()) {
return;
}
// inicialização da atividade
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// solicita-se os números aleatórios
for (int i = 0; i < nbAleas; i++) {
getAlea(a, b);
}
// inicia-se a espera
beginWaiting();
}
@Background(id = "alea")
void getAlea(int a, int b) {
// é preciso fazer o mínimo possível aqui
// em todo caso, nenhuma exibição — estas devem ser feitas no UiThead
try {
// exibimos o resultado no UiThread
showInfo(mainActivity.getAlea(a, b));
} catch (RuntimeException e) {
// a exceção é exibida no UiThread
showAlert(e);
}
}
- linhas 17-18: apaga-se a lista anterior de respostas do servidor. Para isso, na linha 17, esvazia-se a fonte de dados [reponses] associada ao adaptador ListView;
- linha 19: um valor booleano que nos servirá para saber se o usuário cancelou ou não sua solicitação;
- linhas 21-22: exibe-se um contador com valor zero para o número de respostas;
- linhas 24-26: recuperamos os dados inseridos nas linhas [2-6] e verificamos sua validade. Se algum deles for inválido, o método é abortado (linha 25) e o usuário retorna à interface visual;
- linhas 28-29: se todos os dados inseridos forem válidos, transmite-se à atividade o URL do serviço web (linha 28), bem como o tempo de espera antes de cada chamada ao serviço (linha 29). Essas informações são necessárias para a camada [DAO], e vale lembrar que é a atividade que se comunica com ela;
- linhas 31-33: os números aleatórios são solicitados um por um ao método [getAlea] da linha 39;
- linha 38: o método [getAlea] é anotado com a anotação AA [@Background], o que faz com que ele seja executado em outro thread (fluxo de execução, processo) diferente daquele em que a interface visual é executada. De fato, é obrigatório executar qualquer chamada à Internet em um thread diferente daquele da interface visual. Assim, em determinado momento, poderemos ter vários threads:
- aquele que exibe a interface visual UI (User Interface) e gerencia seus eventos,
- os threads [nbAleas], cada um dos quais solicita um número aleatório ao serviço web. Essas threads são iniciadas de forma assíncrona: a thread UI inicia uma thread [getAlea] (linha 32) que solicita um número aleatório ao serviço web e não aguarda sua conclusão. A conclusão será sinalizada por um evento. Assim, as threads [nbAleas] serão iniciadas em paralelo. É possível configurar o aplicativo para que ele inicie apenas uma thread por vez. Nesse caso, há uma fila de espera das threads a serem executadas;
Na linha 38, o parâmetro [id] atribui um nome ao thread gerado. Aqui, todas as threads [nbAleas] têm o mesmo nome: [alea]. Isso nos permitirá cancelá-las todas ao mesmo tempo. Esse parâmetro é opcional caso não se faça o gerenciamento do cancelamento da thread;
- linha 44: o método [getAlea] da atividade é chamado. Ele será, portanto, executado em um thread separado daquele do UI. Este fará a chamada ao serviço web e não aguardará a resposta. Será notificado posteriormente por um evento informando que a resposta está disponível. É nesse momento que, na linha 44, o método [showInfo] será chamado com a resposta recebida como parâmetro;
- linhas 45-47: a execução da solicitação web pode gerar uma exceção. Solicita-se, então, que as mensagens de erro da exceção sejam exibidas em uma mensagem de alerta;
- linha 35: aguarda-se os resultados:
- um indicador de espera será exibido;
- o botão [Annuler] substituirá o botão [Exécuter]. Como as threads iniciadas são assíncronas, a thread do UI não aguarda por elas e a linha 35 é executada antes de elas serem concluídas. Assim que o método [beginWaiting] for concluído, o UI poderá novamente responder às solicitações do usuário, como o clique no botão [Annuler]. Se os threads iniciados fossem síncronos, só chegaríamos à linha 35 depois que todos os threads tivessem sido concluídos. A cancelamento desses threads não faria mais sentido;
O método [showInfo] é o seguinte:
@UiThread
protected void showInfo(int alea) {
if (!hasBeenCanceled) {
// mais uma informação
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// já terminamos?
if (nbInfos == nbAleas) {
// encerramos a espera
cancelWaiting();
}
// adicionamos a informação à lista de respostas
reponses.add(0, String.valueOf(alea));
// exibindo as respostas
adapterReponses.notifyDataSetChanged();
}
}
- o método [showInfo] é chamado dentro do thread [getAlea], anotado por [@Background]. Esse método atualizará a interface visual UI. Isso só é possível se ele for executado dentro da thread do UI. Esse é o significado da anotação [@UiThread] na linha 1;
- linha 2: o método recebe um número aleatório;
- linha 3: o corpo do método só é executado se o usuário não tiver cancelado sua solicitação;
- linhas 5-6: incrementa-se o contador de respostas e exibe-se o valor;
- linhas 8-11: se todas as respostas esperadas tiverem sido recebidas, encerra-se a espera (fim do sinal de espera; o botão [Exécuter] substitui o botão [Annuler]);
- linhas 12-15: adiciona-se o número aleatório recebido à lista de respostas exibida pelo componente [ListView listReponses] e atualiza-se essa lista;
O método [showAlert] é o seguinte:
@UiThread
protected void showAlert(Throwable th) {
if (!hasBeenCanceled) {
// cancela tudo
doAnnuler();
// exibe-se
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
Encontramos uma lógica semelhante à do método [showInfo]:
- linha 1: a anotação [@UiThread] é obrigatória;
- linha 2: o método recebe a exceção que ocorreu;
- linha 3: o método é executado somente se o usuário não tiver cancelado sua solicitação;
- linha 5: a solicitação do usuário é cancelada como se ele próprio tivesse clicado no botão [Annuler];
- linha 7: exibe-se o alerta usando a classe Android [AlertDialog]:
- [activity]: é a atividade do tipo [Activity] armazenada na classe pai [AbstractFragment];
- [setTitle]: define o título da janela de alerta [1];
- [setMessage]: define a mensagem exibida pela janela de alerta [2];
- [setNeutral]: define o botão que fechará a janela de alerta [3];
- [show]: solicita a exibição da janela de alerta;
![]() |
O “clique” no botão [Annuler] é tratado com o seguinte método:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// memória
hasBeenCanceled=true;
// cancela-se a tarefa assíncrona
BackgroundExecutor.cancelAll("alea", true);
// fim da espera
cancelWaiting();
}
- linha 4: registra-se que o usuário cancelou sua solicitação;
- linha 6: cancela todas as tarefas identificadas pela sequência [alea]. O segundo parâmetro [true] significa que elas devem ser canceladas mesmo que já tenham sido iniciadas. O identificador [alea] é aquele utilizado para qualificar o método [getAlea] do fragmento (linha 1 abaixo):
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
Observação: verificou-se que a linha 6 do código do método [doAnnuler] não funcionava corretamente. Por esse motivo, foi adicionada a variável booleana [hasBeenCanceled]. De fato, em caso de exceção (servidor ausente), a janela de alerta era exibida n vezes se tivéssemos solicitado n números aleatórios.
1.16.2.7. A atividade [MainActivity]
![]() |
1.16.2.7.1. A visualização [activity-main.xml]
![]() |
Em comparação com o exemplo anterior, adicionamos uma imagem de espera na visualização associada à atividade [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">
<!-- imagem de espera -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- imagem de espera -->
</android.support.design.widget.AppBarLayout>
...
- linhas 17-21: a imagem de espera;
1.16.2.7.2. A atividade [MainActivity]
A atividade [MainActivity] apresenta poucas alterações em relação ao que era na [Exemple-14]. Em primeiro lugar, injeta-se nela a camada [DAO]:
// injeção dao
@Bean(Dao.class)
protected IDao dao;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// configuração da camada [DAO]
setTimeout(TIMEOUT);
}
- linhas 2-3: injeção da camada [DAO] por meio de uma anotação AA;
- linhas 5-13: código executado após essa injeção;
- linha 12: define-se o timeout da camada [DAO]
Além disso, a atividade [MainActivity] deve implementar a interface [IMainActivity], que, por sua vez, estende a interface [IDao]:
// implementação IMainActivity --------------------------------------------------------------------
@Override
public void navigateToView(int position) {
// exibe-se a visualização de posição
if (mViewPager.getCurrentItem() != position) {
// exibição do fragmento
mViewPager.setCurrentItem(position);
}
}
// gerenciamento da imagem de espera
public void cancelWaiting() {
loadingPanel.setVisibility(View.INVISIBLE);
}
public void beginWaiting() {
loadingPanel.setVisibility(View.VISIBLE);
}
// implementação IDao --------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// execução
return dao.getAlea(a, b);
}
@Override
public void setDelay(int delay) {
dao.setDelay(delay);
}
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
1.16.2.8. Execução do projeto
Inicie o serviço web (parágrafo 1.16.1.7) e, em seguida, inicie o cliente Android:

Para saber o que inserir no [1], proceda da seguinte forma. Abra uma janela de comando e digite o seguinte comando:
C:\Program Files\Console2>ipconfig
Configuration IP de Windows
Carte réseau sans fil Connexion au réseau local* 3 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet VirtualBox Host-Only Network :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::e481:1583:cd2a:c47%27
Adresse IPv4. . . . . . . . . . . . . .: 192.168.82.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet VirtualBox Host-Only Network #2 :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::8191:14ad:407d:b840%54
Adresse IPv4. . . . . . . . . . . . . .: 192.168.64.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet Ethernet :
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Adresse IPv6 de liaison locale. . . . .: fe80::d972:ad53:3b8a:263f%28
Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
Masque de sous-réseau. . . . . . . . . : 255.255.0.0
Passerelle par défaut. . . . . . . . . : 172.19.0.254
Carte réseau sans fil Wi-Fi :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . : uang ad.univ-angers.fr univ-angers.fr
Se você instalou o [GenyMotion], a máquina virtual VirtualBox adicionou endereços IP ao seu computador (linhas 10 e 18). Esses endereços são particularmente práticos, pois não são bloqueados pelo firewall do Windows. A linha 30 fornece o endereço IP do seu computador em uma rede local. Para usar esse endereço, geralmente é necessário desativar o firewall do Windows. Se você estiver conectado a uma rede Wi-Fi, use o endereço Wi-Fi e, nesse caso também, desative o firewall, caso tenha um.
Teste o aplicativo nos seguintes casos:
- 100 números aleatórios no intervalo [1000, 2000] sem tempo de espera;
- 2.000 números aleatórios no intervalo [10000, 20000] sem tempo de espera e cancele a espera antes do término da geração;
- 5 números aleatórios no intervalo [100, 200] com um tempo de espera de 5.000 ms e cancele a espera antes do término da geração;
1.16.2.9. Gerenciamento da cancelamento
Para acompanhar o que ocorre quando o usuário solicita a cancelamento ou quando esta é solicitada devido a uma exceção, adicionamos o seguinte método à interface [IDao] (ver parágrafo 1.16.2.4.1):
package exemples.android.dao;
public interface IDao {
...
// modo de depuração
void setDebugMode(boolean isDebugEnabled);
}
Na classe [Dao], adicionamos o código a seguir:
// modo de depuração
private boolean isDebugEnabled;
// nome da classe
private String className;
..
// construtor
public Dao() {
// nome da classe
className = getClass().getSimpleName();
}
...
// interface IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// execução do serviço
Response<Integer> info;
...
@Override
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
- linha 9: anotamos o nome da classe;
- linhas 16-18: registramos um log sempre que o método [getAlea] é chamado;
Além disso, no fragmento [Vue1Fragment], adicionamos os seguintes registros:
@UiThread
protected void showInfo(int alea) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
....
}
@UiThread
protected void showAlert(Throwable th) {
// registro
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
...
}
}
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// registro
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
...
}
Sempre que o fragmento [Vue1Fragment] recebe uma informação da camada [DAO], é gerado um registro. Além disso, quando o método [doAnnuler] é chamado, o evento é registrado.
Teste 1
Solicitamos 5 números, embora o servidor não tenha sido iniciado. Temos os seguintes registros:
- linhas 1-5: o método [getAlea] da classe [Dao] é chamado cinco vezes. Vale lembrar que se trata de chamadas assíncronas feitas pelo fragmento [VueFragment] e que este não aguarda o resultado de sua chamada;
- linha 7: a primeira solicitação HTTP ocorreu e o fragmento [VueFragment] recebeu sua primeira exceção;
- linha 8: ele então solicita o cancelamento de todas as solicitações;
- linhas 9-12: observa-se, no entanto, que ele recebe as quatro exceções a seguir. Portanto, todas as solicitações assíncronas que estavam em espera foram executadas;
Teste 2
Agora, vamos iniciar o servidor e solicitar 5 números com um intervalo de 5 segundos e clicar em [Annuler] antes do término desse intervalo. Os logs são os seguintes:
- linhas 1-5: o método [getAlea] da classe [Dao] é chamado cinco vezes;
- linha 7: o usuário solicitou o cancelamento das solicitações;
- linha 8: vemos que [Vue1_Fragment] recebe 5 valores. Mais uma vez, todas as solicitações assíncronas que estavam em espera foram executadas;
É por isso que tivemos que lidar com um booleano [hasBeenCanceled] para evitar exibir qualquer coisa quando um cancelamento tivesse sido solicitado. No código do cancelamento:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// memória
hasBeenCanceled = true;
// a tarefa assíncrona foi cancelada
BackgroundExecutor.cancelAll("alea",true);
// fim da espera
cancelWaiting();
}
o código da linha 10 não faz o que se espera. É possível que isso ocorra porque as tarefas assíncronas compartilham o mesmo método anotado [@Background]:
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
1.17. Exemplo-16: gerenciar a assincronia com RxAndroid
Propomos agora gerenciar a assincronia necessária para aplicativos Android com uma biblioteca chamada RxJava [http://reactivex.io/] e sua versão derivada para o ambiente Android, [RxAndroid]. Para isso, utilizaremos o curso [Introduction à RxJava. Application aux environnements Swing et Android].
1.17.1. Criação do projeto
Duplicamos o projeto [Exemple-1] em [Exemple-16]:
![]() | ![]() |
1.17.2. Configuração do Gradle
![]() |
No [build.gradle], adicionamos a dependência da biblioteca [RxAndroid]:
dependencies {
...
compile 'io.reactivex:rxandroid:1.2.0'
}
1.17.3. A camada [DAO]
![]() |
1.17.4. A interface [IDao]
A interface [IDao] passa a ser a seguinte:
package exemples.android.dao;
import rx.Observable;
public interface IDao {
// número aleatório
Observable<Integer> getAlea(int a, int b);
// URL do serviço web
void setUrlServiceWebJson(String url);
// tempo máximo de espera (ms) pela resposta do servidor
void setTimeout(int timeout);
// tempo de espera do cliente, em milissegundos, antes da solicitação
void setDelay(int delay);
// modo de depuração
void setDebugMode(boolean isDebugEnabled);
}
- linha 8: o método [getAlea] agora retorna um tipo [Observable] da biblioteca RxJava (linha 3). O princípio é o seguinte:
Um fluxo de elementos do tipo Observable<T> é observado por um ou mais assinantes (assinantes, observadores, consumidores) do tipo Subscriber<T>. A biblioteca RxJava permite que o fluxo Observable<T> seja executado em um thread T1 e seu observador Subscriber<T> em um thread T2, sem que o desenvolvedorprecise se preocupar em gerenciar o ciclo de vida dessas threads e com problemas naturalmente complexos, como o compartilhamento de dados entre threads e a sincronização entre elas para executar uma tarefa global. Assim, ela facilita a programação assíncrona.
1.17.5. A classe [AbstractDao]
Vamos derivar a classe [Dao] da seguinte classe [AbstractDao]:
package exemples.android.dao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;
public abstract class AbstractDao {
// mapeador jSON
private ObjectMapper mapper = new ObjectMapper();
// métodos protegidos ----------------------------------------------------------
// interface genérica
protected interface IRequest<T> {
Response<T> getResponse();
}
// solicitação genérica
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// execução de serviço
return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// execução do serviço
try {
// a solicitação é feita de forma síncrona e a resposta é encaminhada ao assinante
Response<T> response = request.getResponse();
// erro?
int status = response.getStatus();
if (status != 0) {
// registra-se a exceção
ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
} else {
// envia-se a resposta
subscriber.onNext(response.getBody());
// é sinalizado o fim do observável
subscriber.onCompleted();
}
} catch (JsonProcessingException | RuntimeException e) {
// registra-se a exceção
ex = new DaoException(e, 100);
}
// exceção?
if (ex != null) {
// emite-se a exceção
subscriber.onError(ex);
}
}
});
}
}
- a classe [AbstractDao] tem como elemento principal um método genérico [getResponse] que serve para obter do servidor um tipo [Response<T>], em que T é o tipo do resultado desejado pelo cliente HTTP (neste caso, Integer);
- linha 20: o único parâmetro do método genérico [getResponse] é uma instância da interface genérica [IRequest<T>] das linhas 15 a 17. Essa interface possui apenas um método, [getResponse], e é esse método que fornece a resposta desejada, [Response<T>];
- graças aos dois elementos anteriores, a classe [AbstractDao] pode servir como classe pai para qualquer camada [Dao] cliente de um servidor que envie respostas do tipo [Response<T>];
- linha 20: o método genérico [getResponse] retorna um tipo [Observable<T>] que representa o resultado realmente esperado pelo cliente HTTP (neste caso, um tipo Observable<Integer>);
- linhas 22-51: o método estático [rx.Observable.create] cria um tipo [Observable];
- linha 22: o único parâmetro desse método é uma instância do tipo [rx.Observable.OnSubscribe<T>], uma interface que possui os seguintes métodos:
- [onNext(T element)]: permite enviar a um observador um elemento do tipo T;
- [onError(Throwable th)]: permite emitir uma exceção para um observador;
- [onCompleted]: permite indicar a um observador o fim das emissões;
Um tipo [Observable<T>] obedece a certas restrições:
- ele transmite seus elementos com o método [onNext(T element)];
- o método [onCompleted] deve ser chamado uma única vez assim que não houver mais elementos a serem transmitidos ao observador;
- o método [onCompleted] não é chamado se o método [onError(Throwable th)] já tiver sido chamado;
No nosso exemplo:
- o observador será o fragmento [Vue1Fragment]. É ele que processa os elementos emitidos pelo [Observable<T>] (elemento ou exceção);
- o tipo [Observable<T>] criado emitirá apenas um único elemento (linha 37);
- linha 29: realiza uma consulta síncrona HTTP ao servidor e obtém o tipo [Response<T>]. Essa solicitação HTTP é realizada pelo tipo [IRequest] passado como parâmetro ao método genérico [getResponse];
- linha 31: recupera-se o status da resposta;
- linhas 32-34: se esse status for de um erro, prepara-se uma exceção;
- linhas 36-39: se esse status não for de um erro, então emite-se a resposta realmente esperada pelo cliente (linha 37) e indica-se ao observador que não haverá mais outras emissões (linha 39);
- linhas 41-44: se a solicitação HTTP resultar em uma exceção, ela é registrada;
- linhas 46-49: se a exceção [ex] for diferente de null, então ela é enviada ao observador. Não há necessidade de chamar o método [onCompleted] para indicar ao observador que não haverá mais emissões de elementos. Isso está implícito;
O que fica claro a partir dessas explicações é que:
- o método genérico [<T> Observable<T> getResponse(final IRequest<T> request)] retorna um tipo [Observable<T>] que emite apenas um elemento do tipo T ou uma exceção;
- que esse método admite como único parâmetro um tipo [IRequest<T>], cujo único método [getResponse()] realiza o acesso HTTP, que retorna o tipo [Response<T>];
1.17.6. A classe [Dao]
A classe [Dao] evolui da seguinte forma:
@EBean
public class Dao extends AbstractDao implements IDao {
// cliente do serviço REST
@RestService
protected WebClient webClient;
// tempo de espera antes da execução da solicitação
private int delay;
// modo de depuração
private boolean isDebugEnabled;
// nome da classe
private String className;
// construtor
public Dao() {
// nome da classe
className = getClass().getSimpleName();
}
// interface IDao -------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// execução do cliente web
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// espera
waitSomeTime(delay);
// chamada síncrona HTTP
return webClient.getAlea(a, b);
}
});
}
...
- linha 2: a classe [Dao] estende a classe [AbstractDao];
- linha 24: o método [getAlea] agora retorna um tipo [Observable<Integer>];
- linha 30: chamada do método genérico [getResponse] da classe pai. É passado a ele um parâmetro do tipo [IRequest<Integer>];
- linhas 32-37: implementação da interface [IRequest<Integer>];
- linha 36: realiza-se a consulta HTTP por meio da interface AA [webClient], como havia sido feito anteriormente. Sabemos que vamos recuperar um tipo [Response<Integer>], que é exatamente o tipo que o método [IRequest<Integer>.getReponse()] deve retornar;
- linha 36: aqui utilizamos uma propriedade chamada closure: a capacidade de encapsular em uma instância valores externos a ela no momento de sua criação, neste caso, os valores de [a, b] da linha 24. É isso que permite que o método [IRequest<Integer>.getReponse()] não tenha parâmetros. Estes foram gravados no corpo do método. E onde normalmente se alterariam os parâmetros do método (a, b) -> (x, y), aqui cria-se uma nova instância de [IRequest<Integer>] encapsulando os valores de x e y;
1.17.7. A classe [MainActivity]
A classe [MainActivity], que implementa a interface [IDao], evolui da seguinte forma:
// implementação de IDao --------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(int a, int b) {
// execução
return dao.getAlea(a, b);
}
1.17.8. A classe [Vue1Fragment]
A classe [Vue1Fragment] sofre as seguintes alterações:
@Click(R.id.btn_Executer)
protected void doExecuter() {
// as respostas anteriores são apagadas
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// zeramos o contador de respostas
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// verifica-se a validade dos dados inseridos
if (!isPageValid()) {
return;
}
// inicialização da atividade
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// solicita-se os números aleatórios
getAleasInBackground(a, b);
// inicia-se a espera
beginWaiting();
}
- linha 18: os números aleatórios são solicitados ao método [getAleasInBackground], assim denominado porque os números serão solicitados em um thread diferente daquele da interface do usuário;
private int nbReponses = 0;
// assinaturas dos observáveis
private List<Subscription> abonnements;
// anotação [Background] desnecessária
void getAleasInBackground(int a, int b) {
// inicialmente, sem respostas e sem assinaturas
nbReponses = 0;
abonnements.clear();
// preparando o observável
Observable<Integer> response = Observable.empty();
// estamos mesclando os resultados das diferentes chamadas HTTP
// eles são executados em um thread de E/S
for (int i = 0; i < nbAleas; i++) {
response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
}
// o observável acumulado será observado na thread do UI
response = response.observeOn(AndroidSchedulers.mainThread());
try {
// o observável é executado
abonnements.add(response.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
// a informação é adicionada à lista de respostas
showInfo(alea);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
// mensagem de erro
showAlert(th);
// fim da espera
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// fim da espera
cancelWaiting();
}
}));
} catch (RuntimeException e) {
// a exceção é exibida no UiThread
showAlert(e);
}
}
- linha 3: um observável possui assinantes. A ligação entre um assinante e o processo que ele observa é chamada de assinatura (Subscription). Aqui, teremos apenas um processo observado e um assinante. Portanto, teremos apenas uma assinatura. Por princípio, agimos como se pudéssemos ter vários processos observados por diferentes observadores, o que resultaria em várias assinaturas;
- linhas 11-18: configura-se o processo observado (observável). É importante entender que se trata apenas de uma configuração: o processo não é executado;
- linha 11: parte-se de um observável vazio, um observável que não emite nada;
- linhas 14-16: a esse observável vazio, adicionam-se os observáveis [nbAleas], que serão as consultas [nbAleas] que retornarão números aleatórios [nbAleas];
- linha 15: como anteriormente, o número aleatório n.º i é solicitado à classe [MainActivity]. É importante entender que, neste momento, nenhuma consulta HTTP foi executada ainda. O método [mainActivity.getAlea(a, b)] é executado e retorna um tipo [Observable<Integer>]. Esse é um processo que será observado quando for iniciado;
- linha 15: o método [subscribeOn(Schedulers.io())] solicita que o processo seja executado (quando for) em um thread de E/S. A biblioteca RxJava oferece diferentes tipos de thread. O de E/S é adequado para as chamadas HTTP;
- linha 15: o observável nº i é mesclado ao observável inicial da linha 11: a partir de observáveis [nbAleas], cada um emitindo um elemento, cria-se um observável que emitirá [nbAleas] elementos. É ele que será observado. Esse observável emite a notificação [onCompleted] quando todos os observáveis que o compõem tiverem emitido suas próprias notificações [onCompleted]. Isso nos poupará de ter que contar as respostas, como fizemos na versão anterior, para saber se recebemos todos os números esperados;
- linha 18: ao chegarmos aqui, configuramos um observável que é a composição de [nbAleas] observáveis, cada um sendo executado em um thread de E/S;
- linha 18: o método [observeOn(AndroidSchedulers.mainThread())] serve para indicar em qual thread deve ocorrer a observação dos valores emitidos pelo observável. Aqui, a thread [AndroidSchedulers.mainThread())] pertence à biblioteca RxAndroid e não à RxJava. Ele designa o thread da interface do usuário, também chamado de event loop. Esse ponto é importante: em um aplicativo Android, a modificação de um componente da interface do usuário só pode ser feita no thread da interface do usuário; caso contrário, ocorre uma exceção;
- linhas 19-45: agora que o processo a ser observado foi configurado, ele é executado;
- linha 21: é a operação [Observable.subscribe] que inicia a execução do processo observado. Essa operação iniciará os processos assíncronos [nbAleas] configurados anteriormente. Os resultados desses processos serão automaticamente disponibilizados ao observador na thread da interface do usuário;
- lembramos que o observável emite três tipos de eventos:
- [onNext]: quando emite um elemento;
- [onError]: quando ele encontra uma exceção;
- [onCompleted]: quando sinaliza que não emitirá mais;
O método [Observable.subscribe] tem como parâmetros três objetos [Action1<Integer>, Action1<Throwable>, Action0], cujos métodos [call] servem para tratar cada um desses três eventos;
- linhas 21-27: o primeiro parâmetro do tipo [Action1<Integer>] serve para processar o evento [onNext]. Seu método [call] recebe o elemento que foi emitido pelo observável (linha 23);
- linha 25: reutiliza-se o método [showInfo] do exemplo anterior;
- linhas 27-35: o segundo parâmetro do tipo [Action1<Throwable>] serve para processar o evento [onError]. Seu método [call] recebe a exceção emitida pelo observável (linha 29);
- linha 31: reutiliza-se o método [showAlert] do exemplo anterior;
- linha 33: inicia-se o procedimento de cancelamento da solicitação do usuário. Isso consistirá em cancelar todos os observáveis que estão em execução;
- linhas 35-41: o terceiro parâmetro do tipo [Action0] serve para processar o evento [onCompleted]. Seu método [call] não recebe nenhum parâmetro;
- linha 39: cancela-se a espera;
O método [showInfo] evolui da seguinte forma:
// anotação [UiThread] desnecessária
protected void showInfo(int alea) {
// registro
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
if (!hasBeenCanceled) {
// mais uma informação
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// adicionamos a informação à lista de respostas
reponses.add(0, String.valueOf(alea));
// exibindo as respostas
adapterReponses.notifyDataSetChanged();
}
}
O método apresenta duas alterações:
- linha 1: removeu-se a anotação AA [@UiThread];
- não se contam mais as respostas para determinar se a espera deve ou não ser interrompida. Agora, é o evento [onCompleted] do observável que nos fornece essa informação;
O método [showAlert] sofreu as seguintes alterações:
// anotação [UiThread] desnecessária
protected void showAlert(Throwable th) {
// registro
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
if (!hasBeenCanceled) {
// cancelamos tudo
doAnnuler();
// vamos publicar
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
- a única alteração está na linha 1: removemos a anotação AA [@UiThread];
Por fim, o método [doAnnuler] sofre as seguintes alterações:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// registro
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// memória
hasBeenCanceled = true;
// cancelando as tarefas assíncronas
if (abonnements != null) {
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
}
// fim da espera
cancelWaiting();
}
- linha 12: cancela uma assinatura e, consequentemente, a observação do processo associado;
1.17.9. Execução
Inicie o serviço web (parágrafo 1.16.1.7), inicie o cliente Android e repita os testes que você realizou com o exemplo anterior (parágrafo 1.16.2.8).
1.17.10. Gerenciamento do cancelamento
Repetimos os mesmos testes do exemplo anterior (parágrafo 1.16.2.9).
Teste 1
Solicitamos 5 números, embora o servidor não tenha sido iniciado. Obtemos os seguintes logs:
Após a linha 7, não há mais registros, o que mostra que o observador (Vue1Fragment) não recebe mais notificações do processo observado.
Teste 2
Agora, vamos iniciar o servidor e solicitar 5 números com um intervalo de 5 segundos e clicar em [Annuler] antes do fim desse intervalo. Os registros são os seguintes:
Após a linha 6, não há mais registros, o que mostra que o observador (Vue1Fragment) não recebe mais notificações do processo observado.
Esse é o comportamento esperado de uma cancelamento. Portanto, no código de [Vue1Fragment], podemos remover a variável booleana [hasBeenCanceled] que havíamos introduzido no exemplo anterior, pois o cancelamento não estava funcionando como esperado.
O fato de o observador não receber mais notificações após o cancelamento do observável não significa que as solicitações HTTP sejam, por si só, canceladas. Para comprovar isso, modificamos a classe [Dao] da seguinte maneira:
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// registro
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// execução do cliente web
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// espera
waitSomeTime(delay);
// chamada síncrona HTTP
Response<Integer> response= webClient.getAlea(a, b);
if (isDebugEnabled) {
try {
Log.d(String.format("%s", className), String.format("response [%s]", new ObjectMapper().writeValueAsString(response)));
} catch (JsonProcessingException e) {
Log.d(String.format("%s", className),"erreur désérialisation jSON");
}
}
return response;
}
});
}
- linhas 15-21: registramos o resultado da consulta HTTP da linha 14;
Os registros para o teste nº 2 são, então, os seguintes:
- linhas 1-5: as 5 solicitações foram feitas;
- linha 6: o usuário cancelou;
- linhas 7-11: recebemos corretamente as respostas das cinco solicitações HTTP. No entanto, devido ao cancelamento do observável, esses elementos não são transmitidos ao observador;
1.17.11. Conclusão
No restante deste documento, as aplicações cliente/servidor serão implementadas com a biblioteca RxAndroid, em vez da biblioteca AA, pelos seguintes motivos:
- A biblioteca RxAndroid pode ser utilizada em um aplicativo Android que não utilize a biblioteca AA;
- A RxAndroid faz mais do que apenas facilitar as operações assíncronas. Ela oferece inúmeros métodos para criar um novo observável a partir de outro. Esses métodos não têm equivalente na AA;
- assim que se deseja derivar uma classe anotada por AA, como um fragmento, surgem sérios problemas. Nesse caso, é necessário abandonar o AA e utilizar a solução 1 para a programação assíncrona;
O leitor interessado em aprofundar as possibilidades da biblioteca RxAndroid poderá consultar o documento [Introduction à RxJava. Application aux environnements Swing et Android]. Nele, utiliza-se RxAndroid sem a biblioteca AA.
1.18. Exemplo 17: componentes de entrada de dados
Vamos criar um novo projeto para apresentar alguns componentes comuns em formulários de entrada de dados.
1.18.1. Criação do projeto
Vamos duplicar o projeto [Exemple-13] em [Exemple-17]:
![]() | ![]() |
O novo projeto terá apenas uma visualização, [vue1.xml]. Portanto, excluímos a visualização [vue2.xml] e seus fragmentos associados [Vue2Fragment] e [2]. Levamos em conta essa alteração no gerenciador de fragmentos de [Mainactivity]:
// nosso gerenciador de fragmentos deve ser redefinido para cada aplicativo
// deve definir os seguintes métodos: getItem, getCount, getPageTitle
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// os fragmentos
private final Fragment[] fragments = {new Vue1Fragment_()};
....
}
Execute o projeto novamente. A visualização nº 1 deve aparecer como antes. Vamos trabalhar a partir desse projeto.
1.18.2. A visualização XML do formulário
![]() |
A visualização gerada pelo arquivo [vue1.xml] é a seguinte:

O texto XML da visualização é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="30dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textViewFormulaireTitre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="10dp"
android:layout_marginTop="30dp"
android:text="@string/titre_vue1"
android:textSize="30sp"/>
<Button
android:id="@+id/formulaireButtonValider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/TextViewFormulaireCombo"
android:layout_below="@+id/TextViewFormulaireCombo"
android:layout_marginTop="30dp"
android:text="@string/formulaire_valider"/>
<TextView
android:id="@+id/textViewFormulaireCheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireTitre"
android:layout_below="@+id/textViewFormulaireTitre"
android:layout_marginTop="30dp"
android:text="@string/formulaire_checkbox"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireCheckBox"
android:layout_below="@+id/textViewFormulaireCheckBox"
android:layout_marginTop="30dp"
android:text="@string/formulaire_radioButton"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireSeekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireRadioButton"
android:layout_below="@+id/textViewFormulaireRadioButton"
android:layout_marginTop="30dp"
android:text="@string/formulaire_seekBar"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireEdtText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireSeekBar"
android:layout_below="@+id/textViewFormulaireSeekBar"
android:layout_marginTop="30dp"
android:text="@string/formulaire_saisie"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireBool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireEdtText"
android:layout_below="@+id/textViewFormulaireEdtText"
android:layout_marginTop="30dp"
android:text="@string/formulaire_bool"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireDate"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:layout_alignLeft="@+id/textViewFormulaireBool"
android:layout_below="@+id/textViewFormulaireBool"
android:layout_marginTop="50dp"
android:gravity="center"
android:text="@string/formulaire_date"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireMultilignes"
android:layout_width="150dp"
android:layout_height="100dp"
android:gravity="center"
android:layout_alignBaseline="@+id/textViewFormulaireTitre"
android:layout_alignParentTop="true"
android:layout_marginLeft="400dp"
android:layout_toRightOf="@+id/textViewFormulaireTitre"
android:text="@string/formulaire_multilignes"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireTime"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:gravity="center"
android:layout_alignLeft="@+id/textViewFormulaireMultilignes"
android:layout_below="@+id/textViewFormulaireMultilignes"
android:layout_marginTop="30dp"
android:text="@string/formulaire_time"
android:textSize="20sp"/>
<TextView
android:id="@+id/TextViewFormulaireCombo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireTime"
android:layout_below="@+id/textViewFormulaireTime"
android:layout_marginTop="30dp"
android:text="@string/formulaire_combo"
android:textSize="20sp"/>
<CheckBox
android:id="@+id/formulaireCheckBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireCheckBox"
android:layout_marginLeft="100dp"
android:layout_toRightOf="@+id/textViewFormulaireCheckBox"
android:text="@string/formulaire_checkbox1"/>
<RadioGroup
android:id="@+id/formulaireRadioGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireRadioButton"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:orientation="horizontal">
<RadioButton
android:id="@+id/formulaireRadioButton1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radiobutton1"/>
<RadioButton
android:id="@+id/formulaireRadioButton2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radionbutton2"/>
<RadioButton
android:id="@+id/formulaireRadionButton3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radiobutton3"/>
</RadioGroup>
<SeekBar
android:id="@+id/formulaireSeekBar"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
android:layout_alignLeft="@+id/formulaireCheckBox1"/>
<EditText
android:id="@+id/formulaireEditText1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireEdtText"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:ems="10"
android:inputType="text">
</EditText>
<Switch
android:id="@+id/formulaireSwitch1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireBool"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:text="@string/formulaire_switch"
android:textOff="Non"
android:textOn="Oui"/>
<TimePicker
android:id="@+id/formulaireTimePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormulaireTime"
android:layout_alignLeft="@+id/formulaireEditTextMultiLignes"
android:timePickerMode="spinner"
/>
<EditText
android:id="@+id/formulaireEditTextMultiLignes"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_alignBaseline="@+id/textViewFormulaireMultilignes"
android:layout_alignBottom="@+id/textViewFormulaireMultilignes"
android:layout_marginLeft="50dp"
android:layout_toRightOf="@+id/textViewFormulaireMultilignes"
android:ems="10"
android:inputType="textMultiLine">
</EditText>
<Spinner
android:id="@+id/formulaireDropDownList"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_alignBottom="@+id/TextViewFormulaireCombo"
android:layout_alignLeft="@+id/formulaireEditTextMultiLignes">
</Spinner>
<DatePicker
android:id="@+id/formulaireDatePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormulaireDate"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:datePickerMode="spinner"
android:calendarViewShown="false">
</DatePicker>
<TextView
android:id="@+id/textViewSeekBarValue"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
android:layout_marginLeft="30dp"
android:layout_toRightOf="@+id/formulaireSeekBar"
android:text=""/>
</RelativeLayout>
Os principais componentes do formulário são os seguintes:
| |
| |
| |
| |
| |
| |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
|
1.18.3. As sequências de caracteres do formulário
As sequências de caracteres do formulário estão definidas no seguinte arquivo [res / values / strings.xml]:
![]() |
<resources>
<string name="app_name">Exemple-17</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- visualização 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="formulaire_checkbox">Cases à cocher</string>
<string name="formulaire_radioButton">Boutons Radio</string>
<string name="formulaire_seekBar">Seek Bar</string>
<string name="formulaire_saisie">Champ de saisie</string>
<string name="formulaire_bool">Booléen</string>
<string name="formulaire_date">Date</string>
<string name="formulaire_time">Heure</string>
<string name="formulaire_multilignes">Champ de saisie multilignes</string>
<string name="formulaire_listview">Liste</string>
<string name="formulaire_combo">Liste déroulante</string>
<string name="formulaire_checkbox1">1</string>
<string name="formulaire_checkbox2">2</string>
<string name="formulaire_radiobutton1">1</string>
<string name="formulaire_radionbutton2">2</string>
<string name="formulaire_radiobutton3">3</string>
<string name="formulaire_switch"></string>
<string name="formulaire_valider">Valider</string>
</resources>
1.18.4. O fragmento do formulário
![]() |
A classe [Vue1Fragment] é a seguinte:
package exemples.android.fragments;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.widget.*;
import android.widget.SeekBar.OnSeekBarChangeListener;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
// um fragmento é uma visualização exibida por um contêiner de fragmentos
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// os campos da visualização exibida pelo fragmento
@ViewById(R.id.formulaireDropDownList)
Spinner dropDownList;
@ViewById(R.id.formulaireButtonValider)
Button buttonValider;
@ViewById(R.id.formulaireCheckBox1)
CheckBox checkBox1;
@ViewById(R.id.formulaireRadioGroup)
RadioGroup radioGroup;
@ViewById(R.id.formulaireSeekBar)
SeekBar seekBar;
@ViewById(R.id.formulaireEditText1)
EditText saisie;
@ViewById(R.id.formulaireSwitch1)
Switch switch1;
@ViewById(R.id.formulaireDatePicker1)
DatePicker datePicker1;
@ViewById(R.id.formulaireTimePicker1)
TimePicker timePicker1;
@ViewById(R.id.formulaireEditTextMultiLignes)
EditText multiLignes;
@ViewById(R.id.formulaireRadioButton1)
RadioButton radioButton1;
@ViewById(R.id.formulaireRadioButton2)
RadioButton radioButton2;
@ViewById(R.id.formulaireRadionButton3)
RadioButton radioButton3;
@ViewById(R.id.textViewSeekBarValue)
TextView seekBarValue;
// 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");
}
@SuppressLint("DefaultLocale")
@Click(R.id.formulaireButtonValider)
protected void doValider() {
...
}
@Override
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);
}
}
- linhas 22-49: recuperam-se as referências de todos os componentes do formulário XML [vue1] (linha 18);
- linha 58: o método [setChecked] permite marcar um botão de opção ou uma caixa de seleção;
- linha 60: por padrão, o componente [DatePicker] exibe um campo de entrada de data e um calendário. A linha 60 remove o calendário;
- linha 62: [SeekBar].setMax() permite definir o valor máximo da barra de ajuste. O valor mínimo é 0;
- linhas 63-74: gerenciamos os eventos da barra de ajuste. Queremos, a cada alteração feita pelo usuário, exibir o valor da régua no [TextView] da linha 49;
- linha 71: o parâmetro [progress] representa o valor da régua;
- linhas 76-79: uma lista de [String] que será associada à lista suspensa;
- linha 90: o método [updateFragment] do fragmento. Quando executado, a variável [activity] da classe pai foi inicializada;
- linha 92: a fonte de dados [list] é associada ao adaptador da lista suspensa;
- linhas 93-94: o adaptador [dataAdapter] é associado à lista suspensa [dropDownList];
- linha 84: associa-se o método [doValider] ao clique no botão [Valider];
O método [doValider] tem como objetivo exibir os valores inseridos pelo usuário. Seu código é o seguinte:
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// lista de mensagens a serem exibidas
List<String> messages = new ArrayList<>();
// caixa de seleção
boolean isChecked = checkBox1.isChecked();
messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
// os botões de opção
int id = radioGroup.getCheckedRadioButtonId();
String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
// o SeekBar
int progress = seekBar.getProgress();
messages.add(String.format("SeekBar [value=%d]", progress));
// o campo de entrada
String texte = String.valueOf(saisie.getText());
messages.add(String.format("Saisie simple [value=%s]", texte));
// o botão de alternância
boolean état = switch1.isChecked();
messages.add(String.format("Switch [value=%s]", état));
// a data
int an = datePicker1.getYear();
int mois = datePicker1.getMonth() + 1;
int jour = datePicker1.getDayOfMonth();
messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
// o texto de várias linhas
String lignes = String.valueOf(multiLignes.getText());
messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
// a hora
int heure = timePicker1.getHour();
int minutes = timePicker1.getMinute();
messages.add(String.format("Heure [%d, %d]", heure, minutes));
// lista suspensa
int position = dropDownList.getSelectedItemPosition();
String selectedItem = String.valueOf(dropDownList.getSelectedItem());
messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
// exibição
doAfficher(messages);
}
- linha 4: os valores inseridos serão acumulados em uma lista de mensagens;
- linha 6: o método [CheckBox].isCkecked() permite verificar se uma caixa de seleção está marcada ou não;
- linha 9: o método [RadioGroup].getCheckedButtonId() permite obter o ID do botão de opção que foi marcado ou -1 se nenhum tiver sido marcado;
- linha 10: o código [activity.findViewById(id)] permite identificar o botão de opção marcado e, assim, obter seu texto;
- linha 13: o método [SeekBar].getProgress() permite obter o valor de uma barra de ajuste;
- linha 19: o método [Switch].isChecked() permite determinar se um switch está em On (verdadeiro) ou Off (falso);
- linha 22: o método [DatePicker].getYear() permite obter o ano selecionado por meio de um objeto [DatePicker];
- linha 23: o método [DatePicker].getMonth() permite obter o mês selecionado com um objeto [DatePicker] no intervalo [0,11];
- linha 24: o método [DatePicker].getDayOfMonh() permite obter o dia do mês selecionado com um objeto [DatePicker] no intervalo [1,31];
- linha 30: o método [TimePicker].getHour() permite obter a hora selecionada com um objeto [TimePicker];
- linha 31: o método [TimePicker].getMinute() permite obter os minutos selecionados com um objeto [TimePicker];
- linha 34: o método [Spinner].getSelectedItemPosition() permite obter a posição do elemento selecionado em uma lista suspensa;
- linha 35: o método [Spinner].getSelectedItem() permite obter o objeto selecionado em uma lista suspensa;
O método [doAfficher], que exibe a lista dos valores inseridos, é o seguinte:
private void doAfficher(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));
}
// exibimos
new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
- linha 1: o método recebe uma lista de mensagens a serem exibidas;
- linhas 3-6: um objeto [StringBuilder] é construído a partir dessas mensagens. Para concatenar cadeias de caracteres, o tipo [StringBuilder] é mais eficiente do que o tipo [String];
- linha 8: uma caixa de diálogo exibe o texto da linha 3:

1.18.5. Execução do projeto
Execute o projeto e teste os diferentes componentes de entrada.
1.19. Exemplo 18: uso de um modelo de visualizações
1.19.1. Criação do projeto
Criamos um novo projeto [Exemple-18] por meio da cópia do projeto [Exemple-13].
![]() | ![]() |
1.19.2. O modelo de visões
Queremos recuperar as duas visualizações do projeto e incluí-las em um modelo:
![]() |

Cada uma das duas visualizações será estruturada da mesma forma:
- em [1], um cabeçalho;
- em [2], uma coluna à esquerda que poderia conter links;
- em [3], um rodapé;
- em [4], um conteúdo.
Isso é obtido modificando a visualização básica [activity_main.xml] da atividade;
![]() | ![]() |
O código XML da visualização [main] é o 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>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_marginTop="75dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="0.1"
android:background="@color/lavenderblushh2">
<TextView
android:id="@+id/textViewHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:text="@string/txt_header"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="0.8"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/left"
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="@color/lightcyan2">
<TextView
android:id="@+id/txt_left"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:text="@string/txt_left"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
<exemples.android.architecture.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/floral_white"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</LinearLayout>
<LinearLayout
android:id="@+id/bottom"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="0.1"
android:background="@color/wheat1">
<TextView
android:id="@+id/textViewBottom"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:text="@string/txt_bottom"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
- o cabeçalho [1] é obtido com as linhas 38-54;
- a faixa esquerda [2] é obtida com as linhas 56 a 84;
- o rodapé [3] é obtido com as linhas 86 a 101;
- o conteúdo [4] é obtido com as linhas 78 a 84;
A visualização XML [main] utiliza informações encontradas nos arquivos [res / values / colors.xml] e [res / values / strings.xml]:
![]() |
O arquivo [colors.xml] é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="red">#FF0000</color>
<color name="blue">#0000FF</color>
<color name="wheat">#FFEFD5</color>
<color name="floral_white">#FFFAF0</color>
<color name="lavenderblushh2">#EEE0E5</color>
<color name="lightcyan2">#D1EEEE</color>
<color name="wheat1">#FFE7BA</color>
</resources>
e o arquivo [strings.xml] é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">exemple-12</string>
<string name="action_settings">Settings</string>
<string name="titre_vue1">Vue n° 1</string>
<string name="textView_nom">Quel est votre nom :</string>
<string name="btn_Valider">Validez</string>
<string name="btn_vue2">Vue n° 2</string>
<string name="titre_vue2">Vue n° 2</string>
<string name="btn_vue1">Vue n° 1</string>
<string name="textView_bonjour">"Bonjour "</string>
<string name="txt_header">Header</string>
<string name="txt_left">Left</string>
<string name="txt_bottom">Bottom</string>
</resources>
Crie um contexto de execução para este projeto e execute-o.
1.20. Exemplo 19: o componente [ListView]
O componente [ListView] permite repetir uma determinada visualização para cada elemento de uma lista. A visualização repetida pode ter qualquer nível de complexidade, desde uma simples sequência de caracteres até uma visualização que permita inserir informações para cada elemento da lista. Vamos criar o seguinte [ListView]:

Cada visualização da lista possui três componentes:
- um [TextView] de informações;
- um [CheckBox];
- um [TextView] clicável;
1.20.1. Criação do projeto
Criamos um novo projeto [Exemple-19] duplicando o projeto [Exemple-18].
![]() | ![]() |
![]() |
Vamos dar continuidade ao projeto conforme indicado em [3].
1.20.2. A sessão
![]() |
A sessão armazena os dados compartilhados entre a atividade e os fragmentos:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// uma lista de dados
private List<Data> liste=new ArrayList<>();
// getters e setters
...
}
- linha 11: a lista de dados utilizada pelas duas visualizações;
A classe [Data] é a seguinte:
package exemples.android.architecture;
public class Data {
// dados
private String texte;
private boolean isChecked;
// construtor
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
// getters e setters
...
}
- linha 6: o texto que irá preencher o primeiro [TextView] de cada elemento da lista;
- linha 7: o valor booleano que servirá para marcar ou não o [checkBox] de cada elemento da lista;
1.20.3. A atividade [MainActivity]
O código do método [@AfterInject] fica da seguinte forma:
// injeção de sessão
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// criamos uma lista de dados
List<Data> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new Data("Texte n° " + i, false));
}
}
- linhas 12-15: inicialização da lista de dados presentes na sessão;
1.20.4. A visualização inicial [Vue1]
![]() | ![]() |
A visualização XML [vue1.xml] exibe a área [1] acima. Seu código é o 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:id="@+id/textView_titre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:text="@string/titre_vue1"
android:textSize="50sp" />
<Button
android:id="@+id/button_vue2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textView_titre"
android:layout_below="@+id/textView_titre"
android:layout_marginTop="50dp"
android:text="@string/btn_vue2" />
<ListView
android:id="@+id/listView1"
android:layout_width="600dp"
android:layout_height="200dp"
android:layout_alignParentLeft="true"
android:layout_below="@+id/button_vue2"
android:layout_marginLeft="30dp"
android:layout_marginTop="50dp" >
</ListView>
</RelativeLayout>
- linhas 7-16: o componente [TextView] [2];
- linhas 27-35: o componente [ListView] [4];
- linhas 18-25: o componente [Button] [3];
1.20.5. A visualização repetida pelo [ListView]
![]() |
A visualização repetida pelo [ListView] é a seguinte visualização [list_data]:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/wheat" >
<TextView
android:id="@+id/txt_Libellé"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:text="@string/txt_dummy" />
<CheckBox
android:id="@+id/checkBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/txt_Libellé"
android:layout_marginLeft="37dp"
android:layout_toRightOf="@+id/txt_Libellé"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/textViewRetirer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Libellé"
android:layout_alignBottom="@+id/txt_Libellé"
android:layout_marginLeft="68dp"
android:layout_toRightOf="@+id/checkBox1"
android:text="@string/txt_retirer"
android:textColor="@color/blue"
android:textSize="20sp" />
</RelativeLayout>
- linhas 8-14: o componente [TextView] [1];
- linhas 16-23: o componente [CheckBox] [2];
- linhas 25-35: o componente [TextView] [3];
1.20.6. O fragmento [Vue1Fragment]
![]() |
O fragmento [Vue1Fragment] gerencia a visualização XML [vue1]. Seu código é o seguinte:
package exemples.android.fragments;
import android.view.View;
import android.widget.ListView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// os campos da visualização exibida pelo fragmento
@ViewById(R.id.listView1)
protected ListView listView;
// o adaptador de lista
private ListAdapter adapter;
// inicialização concluída
private boolean initDone = false;
@AfterViews
void afterViews() {
// memória
afterViewsDone = true;
}
@Click(R.id.button_vue2)
void navigateToView2() {
// navega-se para a visualização 2
mainActivity.navigateToView(1);
}
public void doRetirer(int position) {
...
}
@Override
protected void updateFragment() {
if (!initDone) {
// associa-se dados ao [ListView]
adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
initDone = true;
}
// caso em que o fragmento tenha sido (re)gerado — nesse caso, é necessário vincular novamente o ListView ao seu adaptador
listView.setAdapter(adapter);
// caso em que outros fragmentos tenham alterado a fonte de dados — nesse caso, é necessário atualizar o ListView
adapter.notifyDataSetChanged();
}
}
- linha 15: a visualização XML [vue1] está associada ao fragmento;
- linhas 26-30: o método [@AfterViews] não faz nada. No entanto, ele é necessário para definir a variável [afterViewsDone] como true, pois esta é utilizada pela classe pai [AbstractFragment];
- linhas 42-53: o método [updateFragment], que é chamado sempre que o fragmento estiver prestes a ficar visível. O método foi escrito aqui como se o fragmento pudesse sair da adjacência do fragmento exibido e, portanto, reiniciar seu ciclo de vida. Esse não é o caso aqui, mas seria se o aplicativo tivesse três fragmentos com adjacência igual a 1;
- linha 44: o adaptador do [ListView] precisa ser inicializado apenas uma vez;
- linha 46: associamos a este [ListView] um adaptador do tipo [ListAdapter]. Vamos criar essa classe. Ela deriva da classe [ArrayAdapter], que já tivemos a oportunidade de usar para associar dados a um [ListView]. Passamos diversas informações ao construtor do [ListAdapter]:
- uma referência à atividade atual,
- o identificador da visualização que será instanciada para cada elemento da lista,
- uma fonte de dados para alimentar a lista,
- uma referência ao fragmento. Essa referência será usada para gerenciar o clique em um link [Retirer] do [ListView] por meio do método [doRetirer] da linha 38;
- linha 50: o adaptador é associado ao [ListView]. Ao mesmo tempo, a fonte de dados [listes] é associada ao [ListView]. Essa operação será realizada aqui sempre que a visualização nº 1 for exibida. Na verdade, ela só precisaria ser realizada quando o método [@AfterViews] fosse executado. Aqui, a instrução é executada com muita frequência. Percebe-se a necessidade de uma variável booleana que nos indique que o método [@AfterViews] acabou de ser executado e que, portanto, o [ListView] deve ser novamente associado ao seu adaptador;
- linha 52: atualiza-se o [ListView]. Neste exemplo, isso não serve para nada, pois apenas a visualização nº 1 pode modificar a fonte de dados do [ListView]. Consideremos um caso mais geral, em que a visualização nº 2 também poderia alterar a fonte de dados do [ListView]. Encontraremos exemplos desse tipo mais adiante neste documento. Nesse caso, ao passar da visualização nº 2 para a visualização nº 1, o [ListView] da visualização nº 1 deve ser atualizado;
1.20.7. O adaptador [ListAdapter] do [ListView]
![]() |
A classe [ListAdapter]
- configura a fonte de dados do [ListView];
- gerencia a exibição dos diversos elementos do [ListView];
- gerencia os eventos desses elementos;
Seu código é o seguinte:
package exemples.android.fragments;
import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {
// o contexto de execução
private Context context;
// o ID do layout de exibição de uma linha da lista
private int layoutResourceId;
// os dados da lista
private List<Data> data;
// o fragmento que exibe o [ListView]
private Vue1Fragment fragment;
// o adaptador
final ListAdapter adapter = this;
// fabricante
public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
super(context, layoutResourceId, data);
// as informações são armazenadas
this.context = context;
this.layoutResourceId = layoutResourceId;
this.data = data;
this.fragment = fragment;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
}
- linha 5: a classe [ListAdapter] estende a classe [ArrayAdapter];
- linha 19: o construtor;
- linha 20: não se esqueça de chamar o construtor da classe pai [ArrayAdapter] com os três primeiros parâmetros;
- linhas 22-25: armazenamos as informações do construtor;
- linha 29: o método [getView] será chamado repetidamente pelo [ListView] para gerar a visualização do elemento nº [position]. O resultado [View] gerado é uma referência à visualização criada.
O código do método [getView] é o seguinte:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// cria-se a linha atual do ListView
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// o texto
TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
textView.setText(data.get(position).getTexte());
// a caixa de seleção
CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
checkBox.setChecked(data.get(position).isChecked());
// o link [Retirer]
TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
txtRetirer.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
fragment.doRetirer(position);
}
});
// gerenciamos o clique na caixa de seleção
checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
data.get(position).setChecked(isChecked);
}
});
// formata-se a linha
return row;
}
- linha 2: o método recebe três parâmetros. Vamos usar apenas o primeiro;
- linha 4: criamos a visualização do elemento nº [position]. Trata-se da visualização [list_data], cujo ID foi passado como segundo parâmetro ao construtor. Em seguida, recuperamos as referências dos componentes da visualização que acabamos de instanciar;
- linha 6: recuperamos a referência do [TextView] nº 1;
- linha 7: atribui-se a ela um texto proveniente da fonte de dados que foi passada como terceiro parâmetro ao construtor;
- linha 9: recupera-se a referência do [CheckBox] nº 2;
- linha 10: marca-se ou não com um valor proveniente da fonte de dados do [ListView];
- linha 12: recupera-se a referência do [TextView] nº 3;
- linhas 13-18: gerencia-se o clique no link [Retirer];
- linha 16: é o método [Vue1Fragment].doRetirer que irá gerenciar esse clique. De fato, parece mais lógico que esse evento seja gerenciado pelo fragmento que exibe o [ListView]. Ele possui uma visão geral que a classe [ListAdapter] não possui. A referência do fragmento [Vue1Fragment] foi passada como quarto parâmetro ao construtor da classe;
- linhas 20-25: gerenciamos o clique na caixa de seleção. A ação realizada sobre ela é refletida nos dados que ela exibe. Isso ocorre pelo seguinte motivo. O [ListView] é uma lista que exibe apenas uma parte desses elementos. Assim, um elemento da lista às vezes fica oculto, às vezes é exibido. Quando o elemento nº i deve ser exibido, o método [getView] da linha 2 acima é chamado para a posição nº i. A linha 10 recalculará o estado da caixa de seleção a partir do dado ao qual ela está vinculada. Portanto, é necessário que ela memorize o estado da caixa de seleção ao longo do tempo;
1.20.8. Remover um elemento da lista
O clique no link [Retirer] é tratado no fragmento [Vue1Fragment] pelo seguinte método [doRetirer]:
public void doRetirer(int position) {
// removemos o elemento nº [position] da lista
List<Data> liste = mainActivity.getListe();
liste.remove(position);
// registramos a posição da rolagem para voltar a ela
// ler
// [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// posição do primeiro elemento visível, seja ele totalmente visível ou não
int firstPosition = listView.getFirstVisiblePosition();
// deslocamento no eixo Y desse elemento em relação à parte superior do ListView
// mede a altura da parte eventualmente oculta
View v = listView.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// atualiza-se o [ListView]
adapter.notifyDataSetChanged();
// posiciona-se no local correto do ListView
listView.setSelectionFromTop(firstPosition, top);
}
- linha 1: recebe-se a posição no [ListView] do link [Retirer] que foi clicado;
- linha 3: recupera-se a lista de dados;
- linha 4: remove-se o elemento com o número [position];
- linha 15: atualiza-se o [ListView]. Sem isso, visualmente nada muda.
- linhas 5-13, 17: um processo bastante complexo. Sem ele, ocorre o seguinte:
- o [ListView] exibe as linhas 15 a 18 da lista de dados,
- a linha 16 é excluída,
- a linha 15 acima o reinicializa totalmente e o [ListView] exibe então as linhas 0 a 3 da lista de dados;
Com as linhas acima, a exclusão é realizada e o [ListView] permanece posicionado na linha seguinte à linha excluída.
1.20.9. A visualização XML [Vue2]
![]() | ![]() |
O código XML da visualização é o 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:id="@+id/textView_titre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:text="@string/titre_vue2"
android:textSize="50sp" />
<Button
android:id="@+id/button_vue1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textViewResultats"
android:layout_marginTop="25dp"
android:layout_alignLeft="@+id/textView_titre"
android:text="@string/btn_vue1" />
<TextView
android:id="@+id/textViewResultats"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textView_titre"
android:layout_marginTop="50dp"
android:layout_alignLeft="@+id/textView_titre"
android:text="" />
</RelativeLayout>
- linhas 6-15: o componente [TextView] nº 1;
- linhas 26-33: o componente [TextView] nº 2;
- linhas 17-24: o componente [Button] n.º 3;
1.20.10. O fragmento [Vue2Fragment]
![]() | 123 ![]() |
O fragmento [Vue2Fragment] gerencia a visualização XML [vue2]. Seu código é o seguinte:
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
// os campos da visualização
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@AfterViews
void initFragment(){
// memória
afterViewsDone=true;
}
@Click(R.id.button_vue1)
void navigateToView1() {
// navegamos para a visualização 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// exibe os itens da lista que foram selecionados na visualização 1
StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
for (Data data : mainActivity.getListe()) {
if (data.isChecked()) {
texte.append(String.format("(%s)", data.getTexte()));
}
}
texte.append("]");
txtResultats.setText(texte);
}
}
O código importante está no método [updateFragment], na linha 32:
- linha 34: calcula-se o texto a ser exibido no [TextView] nº 2;
- linhas 35-39: percorre-se a lista de dados exibida pelo [ListView]. Ela está armazenada na atividade;
- linha 36: se o dado nº i tiver sido marcado, adiciona-se o texto associado em um tipo [StringBuilder];
- linha 41: o [TextView] exibe o texto calculado;
1.20.11. Execução
Crie uma configuração de execução para este projeto e execute-a.
1.20.12. Melhoria
No exemplo anterior, utilizamos uma fonte de dados List<Data>, em que a classe [Data] era a seguinte:
package exemples.android.fragments;
public class Data {
// dados
private String texte;
private boolean isChecked;
// fabricante
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
...
}
Na linha 7, utilizamos um valor booleano para gerenciar a caixa de seleção dos elementos do [ListView]. Frequentemente, o [ListView] precisa exibir dados que podem ser selecionados marcando uma caixa de seleção, mesmo que o elemento da fonte de dados não possua um campo booleano correspondente a essa caixa. Nesse caso, pode-se proceder da seguinte maneira:
A classe [Data] passa a ser a seguinte:
package exemples.android.fragments;
public class Data {
// dados
private String texte;
// construtor
public Data(String texte) {
this.texte = texte;
}
// getters e setters
...
}
Cria-se uma classe [CheckedData] derivada da anterior:
package exemples.android.fragments;
public class CheckedData extends Data {
// elemento marcado
private boolean isChecked;
// construtor
public CheckedData(String text, boolean isChecked) {
// pai
super(text);
// local
this.isChecked = isChecked;
}
// getters e setters
...
}
Em seguida, basta substituir em todo o código (MainActivity, ListAdapter, Vue1Fragment, Vue2Fragment), o tipo [Data] pelo tipo [CheckedData]. Por exemplo, em [MainActivity]:
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// criamos uma lista de dados
List<CheckedData> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new CheckedData("Texte n° " + i, false));
}
}
O projeto desta versão é fornecido a você com o nome [Exemple-19B].
1.21. Exemplo 20: usar um menu
1.21.1. Criação do projeto
Duplicamos o projeto [Exemple-19B] no projeto [Exemple-20]:
![]() | ![]() |
![]() | 3 ![]() |
Vamos remover os botões das visualizações 1 e 2 para substituí-los por opções de menu [1-2].
1.21.2. A definição XML dos menus
![]() |
O arquivo [res / menu / menu_vue1] define o menu da visualização nº 1:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuOptions"
app:showAsAction="ifRoom"
android:title="@string/menuOptions">
<menu>
<item
android:id="@+id/actionCacherMontrerTout"
android:title="@string/actionCacherMontrerTout"/>
<item
android:id="@+id/actionCacherMontrerActions"
android:title="@string/actionCacherMontrerActions"/>
<item
android:id="@+id/actionCacherMontrerActionsValider"
android:title="@string/actionCacherMontrerActionsValider"/>
</menu>
</item>
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationVue2"
android:title="@string/navigationVue2"/>
</menu>
</item>
</menu>
Os itens do menu são definidos pelas seguintes informações:
- android:id: o identificador do elemento;
- android:title: o texto do elemento;
- app:showsAsAction: indica se o item do menu pode ser colocado na barra de ações da atividade. [ifRoom] indica que o item deve ser colocado na barra de ações se houver espaço para ele;
- uma opção de menu pode, por sua vez, ser um submenu (tag <menu>, linhas 25, 29);
O arquivo [res / menu / menu_vue2] define o menu da visualização nº 2:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationVue1"
android:title="@string/navigationVue1"/>
</menu>
</item>
</menu>
1.21.3. O gerenciamento do menu na classe abstrata [AbstractFragment]
Vamos factorizar o gerenciamento do menu na classe pai [AbstractFragment] das duas visualizações:
package exemples.android.architecture;
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractFragment extends Fragment {
// dados acessíveis às classes filhas
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
protected String className;
// atividade
protected IMainActivity mainActivity;
protected Activity activity;
// sessão
protected Session session;
// menu
private Menu menu;
private int[] menuOptions;
private boolean initDone;
// construtor
public AbstractFragment() {
// inicialização
className = getClass().getSimpleName();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// memória
this.menu = menu;
// log
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
// recupera-se as # opções do menu, caso ainda não tenha sido feito
if (!initDone) {
// recupera-se as # opções do menu
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// a lista de opções é transferida para uma matriz
menuOptions = new int[menuOptionsIds.size()];
for (int i = 0; i < menuOptions.length; i++) {
menuOptions[i] = menuOptionsIds.get(i);
}
// atividade
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
// memória
initDone = true;
}
// solicita-se que o fragmento filho seja inicializado
updateFragment();
}
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
...
}
// exibição das opções do menu -----------------------------------
protected void setAllMenuOptions(boolean isVisible) {
....
}
protected void setMenuOptions(MenuItemState[] menuItemStates) {
...
}
// atualização da classe filha
protected abstract void updateFragment();
}
- linha 42: os logs mostram que o método [onCreateOptionsMenu] é chamado sempre que o fragmento é exibido. Ele é chamado muito tarde, especificamente depois que o método [updateFragment] foi chamado. Isso sugere que ele poderia ser usado para atualizar o fragmento. É isso que faremos aqui (linha 63);
- linha 42: o método tem dois parâmetros:
- [menu]: que é um menu vazio;
- [inflater]: uma ferramenta que permite criar o menu a partir de sua descrição inicial. Não utilizaremos essa possibilidade aqui, pois usaremos uma anotação AA que fará isso por nós;
- linha 44: armazenamos o menu. Precisaremos dele posteriormente;
- linhas 52-53: armazenamos na matriz da linha 28 os identificadores de todos os elementos do menu;
- linhas 55-57: os logs mostram que, quando o método [onCreateOptionsMenu] é chamado, o método [Fragment.getActivity()] retorna a atividade associada ao fragmento;
- linha 55: armazenamos a atividade como uma instância da classe Android [Activity];
- linha 56: armazenamos a atividade como uma instância da interface [IMainActivity];
- linha 57: armazenamos a sessão;
- linha 59: observamos que a inicialização da classe já foi feita para não precisarmos repeti-la (linha 50);
- linha 63: solicitamos que o fragmento filho seja atualizado. Isso é possível porque o fragmento está ao mesmo tempo visível e associado à sua visualização e ao seu menu;
O método [getMenuOptions], que permite obter os identificadores dos elementos de um menu, é o seguinte:
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// percorre-se 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
if (menuItem.hasSubMenu()) {
// recursividade
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
O método [setAllMenuOptions] permite ocultar/exibir todas as opções do menu;
protected void setAllMenuOptions(boolean isVisible) {
// atualiza-se todas as opções do menu
for (int menuItemId : menuOptions) {
menu.findItem(menuItemId).setVisible(isVisible);
}
}
O método [setMenuOptions] permite ocultar/exibir algumas das opções do menu;
protected void setMenuOptions(MenuItemState[] menuItemStates) {
// atualiza-se algumas opções do menu
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
A classe [MenuItemState] é a seguinte:
![]() |
package exemples.android.architecture;
public class MenuItemState {
// identificador da opção do menu
private int menuItemId;
// visibilidade da opção
private boolean isVisible;
// construtores
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// getters e setters
...
}
1.21.4. O gerenciamento do menu no fragmento [Vue1Fragment]
A classe [Vue1Fragment] passa a ser a seguinte:
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {
...
@OptionsItem(R.id.navigationVue2)
void navigateToView2() {
// navega-se para a visualização 2
mainActivity.navigateToView(1);
}
@OptionsItem(R.id.actionValider)
void valider() {
// exibe-se uma mensagem
Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
}
private boolean actionCacherMontrerTout = true;
@OptionsItem(R.id.actionCacherMontrerTout)
void cacherMontrerTout() {
// mudança de estado
actionCacherMontrerTout = !actionCacherMontrerTout;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuNavigation, actionCacherMontrerTout), new MenuItemState(R.id.menuActions, actionCacherMontrerTout)});
}
private boolean actionCacherMontrerActions = true;
@OptionsItem(R.id.actionCacherMontrerActions)
void actionCacherMontrerActions() {
// mudança de estado
actionCacherMontrerActions = !actionCacherMontrerActions;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
}
private boolean actionCacherMontrerActionsValider = true;
@OptionsItem(R.id.actionCacherMontrerActionsValider)
void actionCacherMontrerActionsValider() {
// altera-se o estado
actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
}
...
@Override
protected void updateFragment() {
....
// atualiza-se o menu
//setMenuOptions(...)
}
}
- linha 2: o menu [res / menu / menu_vue1.xml] é associado ao fragmento;
- linha 48: quando o método [updateFragment] é executado, o menu também pode ser atualizado para refletir o novo estado do fragmento;
- linha 7: a anotação [@OptionsItem(R.id.navigationVue2)] indica o método que deve ser executado ao clicar na opção de menu [Navigation / Vue 2];
- linhas 19-25: para ocultar um ramo do menu, basta ocultar a opção raiz desse ramo;
- linha 24: exibe-se/oculta-se a opção raiz [menuNavigation, menuActions];
- linha 40: para exibir uma opção de um ramo do menu, é necessário não apenas exibi-la, mas também todas as opções encontradas ao subir da opção folha até a raiz do menu;
1.21.5. Gerenciamento do menu no fragmento [Vue2Fragment]
Encontramos um código semelhante no fragmento da visualização nº 2:
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.models.CheckedData;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue2)
@OptionsMenu(R.menu.menu_vue2)
public class Vue2Fragment extends AbstractFragment {
// os campos da visualização
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@OptionsItem(R.id.navigationVue1)
void navigateToView1() {
// navega-se para a visualização 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// exibimos os itens da lista que foram selecionados na visualização 1
StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
for (CheckedData data : session.getListe()) {
if (data.isChecked()) {
texte.append(String.format("(%s)", data.getTexte()));
}
}
texte.append("]");
txtResultats.setText(texte);
// atualiza-se o menu
// setMenuOptions(...)
}
}
- linha 35: exibe-se a opção [Navigation / Vue 1];
- linhas 17-20: ao clicar na opção [Navigation / Vue1], é chamado o método [navigateToView1];
1.21.6. Execução
Crie um contexto de execução para este projeto e execute-o.
1.22. Exemplo 21: refatoração da classe abstrata [AbstractFragment]
O exemplo anterior nos mostrou que, quando o fragmento possui um menu, seu método [onCreateOptionsMenu] é um bom local para solicitar que o fragmento seja atualizado:
- ele é chamado exatamente uma vez quando o fragmento for exibido;
- quando é chamado, as associações do fragmento com sua atividade, sua visualização e seu menu são estabelecidas;
Para demonstrar isso, retomamos o exemplo 12, que se caracteriza por ter muitos fragmentos cuja adjacência pode ser alterada. Nesse exemplo, os fragmentos não possuíam menu. Vamos associar a eles um menu vazio.
1.22.1. Criação do projeto
Duplicamos o projeto [Exemple-12] no projeto [Exemple-21]:
![]() | ![]() |
1.22.2. O menu dos fragmentos
![]() |
O menu adicionado para os fragmentos estará vazio:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
</menu>
O que é preciso entender aqui é que a atividade já possui seu próprio menu [menu_main]:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
Quando uma atividade já possui um menu, o menu associado aos fragmentos é adicionado ao da atividade: assim, temos as opções de dois menus. Neste caso, o menu dos fragmentos ficará vazio. Portanto, veremos apenas o menu da atividade.
1.22.3. Os fragmentos
![]() |
Retomamos a classe abstrata [AbstractFragment] do exemplo anterior (ver parágrafo 1.21.3). Associamos o menu [menu_fragment] aos dois fragmentos:
@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {
Nos dois fragmentos [PlaceholderFragment] e [Vue1Fragment], removemos as referências à antiga classe abstrata [AbstractFragment].
1.22.4. Execução
Execute o aplicativo e verifique se ele funciona. Acompanhe os logs para ver quando o método [onCreateOptionsMenu] da classe [AbstractFragment] é executado. Agora é ele que chama o método [updateFragment] dos fragmentos filhos.
1.23. Exemplo 22: salvamento/restauração do estado da atividade e dos fragmentos
1.23.1. O problema
Abordamos aqui o problema da rotação do dispositivo Android (retrato <--> paisagem). Para ilustrá-lo, retomamos o exemplo 21 anterior:

Se girarmos o dispositivo [1], obtemos a seguinte nova visualização:

Percebemos que:
- em [1], a aba [Fragment n° 3] desapareceu;
- em [2], o texto exibido é, de fato, o do fragmento nº 3, mas o contador de visitas está incorreto;
Durante essa rotação, os logs são os seguintes:
- linha 1: percebe-se que a atividade foi totalmente reconstruída;
- linhas 3-7: o mesmo ocorre com os cinco fragmentos gerenciados pela atividade;
- linha 21: o fragmento nº 3 será exibido. Observa-se que, antes do incremento, o número da visita é 0;
Assim, o resultado obtido após a rotação pode ser explicado da seguinte forma:
- a classe [MainActivity] cria inicialmente uma barra de abas com uma única aba, intitulada [Vue 1]. Essa é a aba que está visível;
- após a rotação do dispositivo, o gerenciador de páginas [mViewPager] exibe novamente o mesmo fragmento, ou seja, neste caso, o fragmento nº 3. É importante lembrar que abas e fragmentos são conceitos diferentes e possuem ciclos de vida distintos. O método [updateFragment] do fragmento nº 3 será executado:
public void updateFragment() {
// registro
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// incrementar o número de visita
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// texto alterado
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- linha 7: o último número da visita é lido na sessão. No entanto, essa sessão, assim como todo o resto, foi reconstruída e o número da visita foi zerado. Isso explica o resultado exibido no fragmento nº 3;
1.23.2. Métodos de salvamento/restauração da atividade e dos fragmentos
1.23.2.1. Solução 1: salvamento manual
Durante a rotação do dispositivo, dois métodos da atividade são chamados:
// gerenciamento de backup/restauração da atividade ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// pai
super.onSaveInstanceState(outState);
// salvamento do estado da atividade
// ....
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// pai
super.onCreate(savedInstanceState);
// restauração da atividade
// ...
}
- linhas 2-8: o método [onSaveInstanceState] é chamado pelo sistema durante a rotação. É nesse ponto que o backup da atividade pode ser feito. Se nada for feito, nada será salvo. O salvamento do estado da atividade deve ser feito no parâmetro [Bundle outState] passado para o método. A classe [Bundle] se assemelha a um dicionário. Ela possui o método [putString, putInt, putLong, putBoolean, putChar, ...] com dois parâmetros: void putT(String key, T value);
- linhas 10-16: o método [onCreate] é chamado durante a criação da atividade. Se o estado da atividade tiver sido salvo, esse salvamento é passado a ele no parâmetro [Bundle savedInstanceState]. Para recuperar os valores salvos, existem métodos como o [getString, getInt, getLong, geBoolean, getChar, ...] com um parâmetro: T getT(String key);
Os fragmentos dispõem desses mesmos dois métodos para salvar seu estado.
Vamos usar essas informações para salvar e restaurar o estado do exemplo 21. Para isso, duplicamos o projeto [Exemple-21] em [Exemple-22].
1.23.2.2. Solução 2: salvamento automático
A documentação do Android indica que, ao girar o dispositivo, é possível evitar a destruição de um fragmento usando a instrução: [Fragment].setRetainInstance(true). Vários artigos do [StackOverflow] recomendam usar essa instrução apenas para fragmentos sem interface visual [http://stackoverflow.com/questions/11182180/understanding-fragments-setretaininstanceboolean, http://stackoverflow.com/questions/12640316/further-understanding-setretaininstancetrue, http://stackoverflow.com/questions/21203948/setretaininstancetrue-in-oncreate-fragment-in-android]. Testei essa instrução em dois exemplos: Exemplo-17 (parágrafo 1.18 — um aplicativo com um fragmento que exibe um formulário) e Exemplo-21 (parágrafo 1.22), um aplicativo com cinco fragmentos. Em ambos os casos, essa única instrução aplicada a todos os fragmentos do aplicativo revelou-se insuficiente para restaurar corretamente a visualização exibida durante a rotação do dispositivo. Em vez de criar dois modelos, um baseado em [setRetainInstance(true)] e outro baseado em [setRetainInstance(false)], que é o valor padrão, decidi seguir as recomendações de [StackOverflow] e manter o valor false como padrão do método [setRetainInstance(boolean )]. A instrução: [Fragment].setRetainInstance(true) nunca foi utilizada no restante deste documento.
1.23.3. O método de salvamento/restauração do projeto [Exemple-22]
O projeto [Exemple-22] evolui da seguinte forma:
![]() |
Nele surgem duas novas classes:
- [PlaceHolderFragmentState], que armazenará o estado de um fragmento do tipo [PlaceHolderFragment];
- [Vue1FragmentState], que armazenará o estado do fragmento do tipo [Vue1Fragment];
Essas classes são as seguintes:
package exemples.android;
public class Vue1FragmentState {
// estado Vue1Fragment
private boolean hasBeenVisited=false;
// getters e setters
...
}
- linha 5: o booleano [hasBeenVisited] é verdadeiro se o fragmento [Vue1Fragment] tiver sido visitado (exibido) pelo menos uma vez. Esse campo foi criado para o exemplo, pois o fragmento [Vue1Fragment] não tem nada a ser salvo;
A classe [PlaceHolderFragmentState] é a seguinte:
package exemples.android;
public class PlaceHolderFragmentState {
// estado visitado ou não
private boolean hasBeenVisited;
// texto exibido
private String text;
// getters e setters
...
}
- linha 5: encontramos o valor booleano [hasBeenVisited];
- linha 7: o texto exibido pelo fragmento no momento em que ele deve ser salvo. Vimos que esse texto foi perdido durante a rotação;
O estado dos fragmentos será armazenado na sessão, e caberá à atividade salvar/restaurar essa sessão. A sessão evolui da seguinte maneira:
package exemples.android;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// número de fragmentos visitados
private int numVisit;
// nº do fragmento do tipo [PlaceholderFragment] exibido na segunda aba
private int numFragment = -1;
// nº da aba selecionada
private int selectedTab = 0;
// nº da visualização atual
private int currentView;
// fragmentos salvos ---------------
private Vue1FragmentState vue1FragmentState;
private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];
// construtor
public Session() {
for (int i = 0; i < placeHolderFragmentStates.length; i++) {
placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
}
vue1FragmentState = new Vue1FragmentState();
}
// getters e setters
...
}
- linha 18: o estado do fragmento [Vue1Fragment];
- linha 19: o estado dos fragmentos do tipo [PlaceHolderFragment];
- linhas 22-27: no construtor da sessão, inicializam-se os campos das linhas 18 e 19;
- linhas 12-15: surgem dois novos campos:
- linha 13: o número da última aba selecionada;
- linha 15: o número do último fragmento exibido;
A atividade salva/restaura a sessão da seguinte maneira:
// gerenciamento de salvamento/restauração da atividade ----------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// pai
super.onSaveInstanceState(outState);
// salvamento de sessão
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();
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// pai
super.onCreate(savedInstanceState);
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();
}
}
}
}
- linha 8: a sessão é salva na forma de sua string jSON;
- linha 29: a sessão é restaurada a partir de sua cadeia jSON;
Para gerenciar o salvamento/restauração dos fragmentos, a classe abstrata [AbstractFragment] é alterada da seguinte forma:
// gerenciamento de salvamento/restauração -----------------------------------------------
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// pai
super.setUserVisibleHint(isVisibleToUser);
// backup?
if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
// o fragmento será armazenado em cache — estamos fazendo o backup
saveFragment();
saveFragmentDone = true;
}
// memória
this.isVisibleToUser = isVisibleToUser;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// pai
super.onActivityCreated(savedInstanceState);
// log
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// o fragmento deve ser restaurado
fragmentHasToBeInitialized = true;
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// registro
if (isDebugEnabled) {
Log.d(className, "onSaveInstanceState");
}
// pai
super.onSaveInstanceState(outState);
// salvar o fragmento somente se ele estiver visível
if (isVisibleToUser && !saveFragmentDone) {
saveFragment();
saveFragmentDone = true;
}
}
// classes filhas
protected abstract void updateFragment();
protected abstract void saveFragment();
- decide-se salvar o estado dos fragmentos na sessão em dois momentos:
- linhas 2-14: quando o fragmento passa de visível para oculto;
- linhas 29-42: quando o sistema indica que é necessário fazer um salvamento do fragmento e este está visível (linha 38);
Esse mecanismo evita que se façam salvamentos com mais frequência do que o necessário. De fato, como salvamos o estado do fragmento i quando ele passou de visível para oculto, quando o fragmento j é exibido e se realiza uma rotação, é desnecessário salvar novamente o fragmento i. Se ele não foi exibido novamente desde seu último salvamento, então seu estado não mudou. Apenas o estado do fragmento j precisa ser salvo. Esse mecanismo também tem outra vantagem: não é apenas durante uma rotação do dispositivo que precisamos salvar o estado de um fragmento. Há também o caso da navegação pura entre fragmentos, por exemplo, em um sistema com abas. Nesse caso, queremos recuperar um fragmento no estado em que o deixamos na última vez em que foi exibido. Esse estado pode ter desaparecido parcialmente se esse fragmento tiver saído, em determinado momento, da adjacência dos fragmentos exibidos. O fragmento, portanto, não é reconstruído em sua totalidade, mas sua visualização associada, sim. O salvamento que foi feito quando o fragmento ficou oculto servirá para recuperar o último estado dessa visualização;
- linhas 10, 40: para evitar fazer dois salvamentos sucessivos, utiliza-se o booleano [saveFragmentDone] para indicar que um salvamento foi feito;
- linhas 9, 39: solicita-se ao fragmento filho que salve seu estado. O método [saveFragment] é abstrato (linha 47). Cabe, portanto, às classes filhas implementá-lo;
- linhas 16-26: o método [onActivityCreated] é utilizado para definir a variável booleana [fragmentHasToBeInitialized] como verdadeira. De fato, o fragmento filho precisa saber que deve reinicializar totalmente o estado do fragmento a partir de um estado que encontrará na sessão;
Ainda na classe [AbstractFragment], o método [onCreateOptionsMenu] é alterado da seguinte forma:
// atualização do fragmento
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// memória
this.menu = menu;
// log
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
...
// solicita-se que o fragmento filho seja atualizado
updateFragment();
// salvamento a ser feito
saveFragmentDone = false;
}
- linha 14: vimos que o booleano [saveFragmentDone] passou para vrai quando foi feito um salvamento. Em determinado momento, ele precisa voltar para faux. Quando o método [updateFragment] (linha 12) do fragmento filho for executado, este se tornará visível. Ora, é quando está visível que um fragmento deve ser salvo, no momento específico em que ele passar do estado visível para o estado oculto. Portanto, define-se a variável booleana [saveFragmentDone] como false para que o salvamento possa ocorrer;
1.23.4. Salvamento do fragmento [Vue1Fragment]
O salvamento dos fragmentos é feito no método [saveFragment], chamado pela classe pai [AbstractFragment]:
// salvamento do estado do fragmento
@Override
public void saveFragment() {
// registro
if (isDebugEnabled) {
Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
}
// salvamento do estado do fragmento na sessão
Vue1FragmentState state = new Vue1FragmentState();
state.setHasBeenVisited(true);
session.setVue1FragmentState(state);
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- linhas 9-11: salvamento do estado do fragmento na sessão. Quando o método [saveFragment] é chamado, o fragmento fica visível. Portanto, é necessário definir o booleano [hasBeenVisited] como vrai (linha 10);
1.23.5. Salvamento do fragmento [PlaceHolderFragment]
O salvamento dos fragmentos é feito no método [saveFragment], chamado pela classe pai [AbstractFragment]:
@Override
public void saveFragment() {
// estado do fragmento está sendo salvo na sessão
PlaceHolderFragmentState state = new PlaceHolderFragmentState();
state.setText(textViewInfo.getText().toString());
state.setHasBeenVisited(true);
session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- linhas 4-7: salvamento do estado do fragmento na sessão;
- linha 5: o texto atualmente exibido pelo [TextView] textViewInfo é salvo;
- linha 6: o valor booleano [hasBeenVisited] do fragmento é alterado para vrai;
- linha 7: o estado do fragmento é registrado na tabela [placeHolderFragmentStates]. O número do elemento a ser inicializado é o número da seção do fragmento menos um;
1.23.6. Restauração do fragmento [Vue1Fragment]
A restauração dos fragmentos é realizada no método [updateFragment]:
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
}
// restauração?
if (fragmentHasToBeInitialized) {
// restauração do estado
hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
fragmentHasToBeInitialized = false;
}
// registro
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
}
// navegação?
boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
if (navigation) {
// incrementar número de visita
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();
}
// alteração do número da visualização atual
session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
}
- linhas 8-12: restauração do estado do fragmento. A variável booleana [fragmentHasToBeInitialized] foi inicializada pela classe pai [AbstractFragment]. Quando seu valor é vrai, o fragmento acaba de ser reconstruído e é necessário reinicializá-lo. É aqui que isso ocorre. Neste exemplo específico, não há nada a ser feito. Apenas mostramos que é possível recuperar o valor da variável booleana [hasBeenVisited] no estado salvo do fragmento (linha 10);
- linha 11: não se deve esquecer de redefinir o [fragmentHasToBeInitialized] para faux, para que, ao retornar posteriormente a este fragmento sem que tenha ocorrido rotação do dispositivo, não se faça uma inicialização desnecessária do fragmento;
- linhas 18-26: incremento do contador de visitas. Aqui, há uma dificuldade: ao restaurar o fragmento, não queremos incrementar esse contador. Precisamos distinguir aqui entre:
- uma simples navegação que leva o usuário de volta à aba [Vue 1];
- uma recarga quando o usuário gira seu dispositivo enquanto a aba [Vue 1] está sendo exibida;
Distinguimos esses dois casos graças ao número da visualização armazenado na sessão. Esse número é o da última visualização exibida (linha 28).
- linha 18: ocorre navegação e não restauração se o número da última visualização for diferente do da visualização atual;
- linhas 21-25: incremento do contador de visitas e sua exibição;
1.23.7. Recuperação do fragmento [PlaceHolderFragment]
A restauração dos fragmentos é feita no método [updateFragment]:
// dados
private String text;
private int numVisit;
private String newText;
private boolean hasBeenVisited = false;
private ObjectMapper jsonMapper = new ObjectMapper();
...
public void updateFragment() {
// registro
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// de qual fragmento se trata?
int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
int numView = numSection - 1;
// o fragmento deve ser inicializado?
if (fragmentHasToBeInitialized) {
// texto inicial
text = getString(R.string.section_format, numSection);
fragmentHasToBeInitialized = false;
}
// navegação?
boolean navigation = session.getCurrentView() != numView;
if (navigation) {
// incrementar o número de visitas
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// texto modificado
newText = String.format("%s, visite %s", text, numVisit);
} else {
// trata-se de uma restauração
PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
newText = state.getText();
}
// exibição de texto
textViewInfo.setText(newText);
// visualização atual
session.setCurrentView(numView);
}
- linhas 15-16: determina-se o número da visualização que está sendo atualizada;
- linhas 18-22: caso em que o fragmento esteja em um ciclo de salvamento/restauração após uma mudança na orientação do dispositivo. Nesse caso, é necessário restaurá-lo. Geralmente, trata-se de restaurar determinados campos do fragmento;
- linha 20: o campo [text] da linha 2 deve conter o texto inicial exibido pelo fragmento: [Hello world from section i]. Aqui, ele deve ser regenerado;
- linha 21: observa-se que a inicialização do fragmento foi realizada;
- linhas 24-36: assim como anteriormente para o fragmento [Vue1Fragment], o incremento do contador de visitas não deve ocorrer durante uma restauração. Como antes, precisamos distinguir entre navegação e restauração;
- linhas 32-36: caso de restauração;
- linha 34: o estado do fragmento antes da rotação do dispositivo é recuperado na sessão;
- linha 35: recupera-se o texto que estava sendo exibido naquele momento;
- linha 38: esse texto é exibido novamente;
- linha 40: registra-se na sessão o número da nova visualização exibida;
1.23.8. Gerenciamento de abas
Os parágrafos anteriores não abordaram o gerenciamento de abas. No entanto, observamos um problema no exemplo 21 durante a rotação do dispositivo: apenas a primeira aba, [Vue 1], foi mantida. A segunda aba, por sua vez, foi perdida.
Resolvemos esse problema na classe [MainActivity] da seguinte maneira:
@AfterViews
protected void afterViews() {
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "afterViews");
}
// barra de ferramentas
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
...
// 1ª aba
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);
}
// qual aba selecionar?
tabLayout.getTabAt(session.getSelectedTab()).select();
...
}
- linhas 14-16: criação da primeira aba;
- linhas 18-23: criação da segunda aba. Para saber se devemos criá-la, verificamos na sessão o número do fragmento exibido na aba 2. Se esse número for diferente de -1, seu valor inicial, então a segunda aba é criada. Nesse estágio, temos duas abas, sendo que, por padrão, a primeira está selecionada;
- linha 26: busca-se na sessão o número da aba que estava selecionada antes do salvamento/restauração e seleciona-se essa novamente. Se o campo [selectedTab] ainda não tiver sido inicializado pelo código, utiliza-se seu valor inicial 0;











































































































































































































































































































































