1. Aprender Programação 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 através de exemplos;
- Controlar um Arduino com um tablet Android;
- Introdução à programação de tablets Android através de exemplos - versão 2
e introduz as seguintes novidades:
- o documento 1 apresentava uma arquitetura denominada AVAT (Atividade-Visualizações-Ações-Tarefas) para facilitar a programação assíncrona numa aplicação Android. Neste documento, a biblioteca padrão RxJava é utilizada para gerir as ações assíncronas;
- o documento 2 utilizava o Eclipse IDE com um plugin Android. Este documento utiliza o Android Studio;
- o documento 3 é reproduzido tal como está;
- o documento 4 utilizava a biblioteca [Android Annotations] (AA) com o IntelliJ IDE Community Edition. Este documento retoma 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, utilizava-se por vezes 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 armazenamento/recuperação da atividade e dos seus fragmentos;
- o ciclo de vida dos fragmentos;
Por fim, apresenta a estrutura básica de um cliente Android que comunica com um serviço web / jSON, na qual se agrupam um grande número de elementos que se encontram regularmente neste tipo de clientes. Esta estrutura básica é utilizada em todos os exemplos a partir do capítulo 2. Esta é a parte verdadeiramente inovadora do documento.
São apresentados os seguintes exemplos:
Natureza | |
Importação de um projeto Android existente | |
Um projeto Android básico | |
Um projeto [Android Annotations] básico | |
Visualizações e eventos | |
Navegação entre vistas | |
Navegação por separadores | |
Utilização da biblioteca [Android Annotations] com o Gradle | |
Gestão de fragmentos numa aplicação Android | |
Navegação entre vistas revista | |
Arquitetura de duas camadas | |
Arquitetura cliente/servidor | |
Gerir a assincronia com RxJava | |
Componentes de introdução de dados | |
Utilização de um modelo de vistas | |
O componente ListView | |
Utilizar um menu | |
Utilização de uma classe pai para os fragmentos | |
Guardar e restaurar o estado da atividade e dos fragmentos | |
Cliente de meteorologia | |
Estrutura de um cliente Android que comunica com um serviço web / jSON. Nela são agrupados vários elementos que se encontram frequentemente neste tipo de clientes Android. | |
Gestão de consultas num consultório médico | |
Exercício prático - Gestão de uma folha de pagamentos básica | |
Exercício prático - Controlo 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 apenas se apresentam as linhas gerais da solução. Cabe ao leitor construir a solução.
O código-fonte dos exemplos está disponível |ICI|. Para executar estes exemplos, deve seguir o procedimento descrito no parágrafo 6.12.
Este documento constitui uma introdução à programação Android. Não pretende ser exaustivo. Destina-se essencialmente a principiantes.
O site de referência para a programação Android encontra-se em URL [http://developer.android.com/guide/components/index.html]. É aí que deve consultar para obter uma visão geral da programação Android.
1.1.2. Pré-requisitos
O pré-requisito para uma utilização ótima deste documento é um bom domínio da linguagem Java.
1.1.3. As ferramentas utilizadas
Os exemplos que se seguem 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, deve instalar:
- um JDK (ver parágrafo 6.8);
- o gestor de emuladores Android Genymotion (ver parágrafo 6.9);
- o gestor 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 Android
1.2.1. Criação do projeto
Vamos criar, com o Android Studio, um primeiro projeto Android. Em primeiro lugar, criemos uma pasta [exemples] vazia onde serão colocados todos os nossos projetos:
![]() |
depois, criemos um projeto com o Android Studio. Vamos, em primeiro lugar, 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 o utilizado aqui para a sua execução. Esta é uma oportunidade para ver como resolver este tipo de erros. Aqui, temos o seguinte erro:
![]() | ![]() |
O projeto importado está configurado pelo seguinte ficheiro [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
// manter o código padrão e o código comum separados do
// do código principal do exemplo.
List<String> dirs = [
'main', // código principal da amostra; procure aqui o que é mais interessante.
'common', // componentes que são reutilizados por vários exemplos
'template'] // código padrão gerado pelo processo de modelos 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 indicado deve-se às linhas 31, 34-35: não temos o SDK 21. Substituímos esta versão pela versão 23, de que dispomos.
No ficheiro [build.gradle], o Android Studio apresenta sugestões como as seguintes:
![]() |
Para aceitar as sugestões, selecionamos [alt-entrée] na sugestão:
![]() |
Também pode ocorrer um erro relacionado com a versão do Gradle:
![]() |
Este erro deve-se a uma incompatibilidade entre a versão do Gradle exigida pelo ficheiro [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 que está indicada no ficheiro [<projet>/gradle/wrapper/gradle-wrapper.properties]:
#Quarta-feira, 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, é necessário substituir 2,8 por 2,10.
Para aceder ao ficheiro [<projet>/gradle/wrapper/gradle-wrapper.properties], é necessário utilizar a perspetiva do projeto:
![]() | ![]() | ![]() |
Depois de corrigido isto, é possível compilar a aplicação [1], iniciar o emulador Genymotion [2] e, em seguida, executar o projeto [3]:
![]() | ![]() | ![]() |
![]() |

Vamos encerrar a aplicação:
![]() |
Agora já podemos fechar o projeto. Vamos criar um novo.
![]() |
1.2.2. Algumas observações sobre o IDE
1.2.2.1. As perspetivas
O Android Studio (AS) oferece várias perspetivas para trabalhar com um projeto. Iremos utilizar principalmente duas:
- a perspetiva [Android] [1]:
- a perspetiva [Project] [4];
![]() | ![]() |
![]() |
Na maioria das vezes, trabalharemos com a perspetiva [Android]. Quando duplicarmos um projeto noutro, precisaremos da perspetiva [Project].
1.2.2.2. Gestão da execução
Existem várias formas de executar / parar / reexecutar um projeto AS. Em primeiro lugar, existem os botões da barra de ferramentas:
![]() | ![]() | ![]() |
O botão [Rerun] [3] interrompe a execução do projeto [2] e, em seguida, reinicia-o [1].
1.2.2.3. Gestão da cache
O Android Studio mantém um cache dos projetos que gere, com o objetivo de tornar o IDE o mais responsivo possível. Com a versão Android 2.1 (maio de 2016), muitas vezes este cache não tinha em conta as alterações de código que acabávamos de fazer. Nesse caso, é necessário invalidar este cache:
![]() | ![]() |
Com o Android 2.1 (maio de 2016), a operação anterior tinha de ser repetida várias vezes e, por vezes, isso não era suficiente para resolver a anomalia detetada. A solução consistiu em desativar a tecnologia [Instant Run]:
![]() | ![]() |
- em [3-4], tendo tudo sido desativado;
Em tudo o que se seguiu, trabalhámos com esta configuração do cache e não encontrámos quaisquer problemas.
1.2.2.4. Gestão dos registos
Durante a execução de um projeto, são apresentados registos no monitor do Android:
![]() | ![]() |
No separador [Android Monitor] [1], os registos são apresentados no separador [logcat] [2]. O botão [3] permite apagar os registos. Este botão é útil quando se pretende ver os registos de uma ação específica:
- apagamos os registos;
- no dispositivo Android, realiza-se a ação cujos registos se pretendem obter;
- os registos que aparecem são os relacionados com a ação realizada;
Existem vários níveis de registos [4]. Por predefinição, está selecionado o modo [Verbose]. Isto significa que são apresentados os registos de todos os níveis. Com o [4], é possível selecionar um nível específico.
Os registos são muito úteis para saber em que momentos da execução de um projeto determinados métodos são apresentados. 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). São chamados em diferentes momentos do ciclo de vida da aplicação. Por vezes, são executados várias vezes. Mesmo ao ler a documentação, por vezes é difícil determinar se um método do ciclo de vida será executado antes ou depois de um método que tenhamos escrito nós próprios. No entanto, esta informação é frequentemente importante de se saber. Podemos, então, inserir registos como se segue:
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]. Esta classe permite registar mensagens de log na consola Android [logcat]. As mensagens de log são classificadas em vários níveis (info, warning, debug, verbose, error). [Log.d] apresenta registos de nível [debug]. O seu primeiro argumento é a fonte da mensagem de registo. Com efeito, várias fontes podem emitir mensagens na consola de registos. Para poder diferenciá-las, utiliza-se este primeiro argumento. O segundo argumento é a mensagem a registar na consola de registos;
Se voltarmos a executar o projeto [Exemple-01], obtemos os seguintes registos:
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
Descobrimos assim que o método [onCreate], que cria a atividade Android, é executado antes do método [onCreateOptionsMenu], que cria o menu da aplicação.
Agora, se clicarmos na opção do menu no emulador Android [1]:
![]() |
o seguinte registo é adicionado à consola de registos:
05-28 08:41:22.881 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onOptionsItemSelected
Daqui em diante, iremos adicionar frequentemente instruções de registo no código Android. Na maioria das vezes, não as iremos comentar. Estão lá apenas para convidar o leitor a consultar a consola de registos, de modo a compreender progressivamente o ciclo de vida de uma aplicação Android.
1.2.2.5. Gestão do emulador [Genymotion]
Por vezes, o emulador Genymotion bloqueia e já não é possível reiniciá-lo. Isto deve-se ao facto de alguns processos do VirtualBox terem permanecido ativos no gestor de tarefas. Abra o gestor de tarefas [Ctrl-Alt-Supp] e elimine todas as tarefas do VirtualBox presentes:
![]() | ![]() |
Depois de fazer isto, reinicie o emulador Genymotion a partir do Android Studio.
1.2.2.6. Gestão do ficheiro binário APK criado
A compilação do projeto produz um ficheiro binário com a extensão .apk:
![]() | ![]() | ![]() |
Existem duas versões: a denominada [debug] e a outra denominada [debug-unaligned]. Deve utilizar-se a primeira, sendo a outra uma versão intermédia. O ficheiro binário .pak gerado no [4] pode ser transferido diretamente para um emulador ou para um dispositivo Android. Para o transferir para um emulador, basta arrastá-lo e soltá-lo no emulador com o rato.
1.3. Exemplo-02: um projeto Android básico
Vamos criar, com o Android Studio, um novo projeto Android [1-12]:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() | ![]() | ![]() |
No [13], executa-se a aplicação. Obtém-se então a visualização [14] no emulador Genymotion.
1.3.1. Configuração do Gradle
O projeto criado é configurado pelo seguinte ficheiro [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'
}
Este ficheiro foi gerado pelo IDE com os elementos da sua configuração. Trata-se de um ficheiro mínimo que iremos ir enriquecendo progressivamente.
- linhas 3-12: as características da aplicação Android;
- linhas 22-25: as suas dependências. É sobretudo aqui que iremos introduzir alterações de acordo com os exemplos estudados;
1.3.2. O manifesto da aplicação
![]() |
O ficheiro [AndroidManifest.xml] [1] define as características do binário da aplicação Android. O 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;
Estas duas informações provêm dos dados introduzidos aquando da criação do projeto:
![]() |
- a linha 3 do manifesto (pacote) provém da entrada [4] acima referida. São geradas automaticamente várias classes neste 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 da aplicação. Faz referência à classe [1] acima;
- linha 6: o ícone [2] da aplicação. Pode ser alterado;
- linha 7: o nome da aplicação. Encontra-se no ficheiro [strings.xml] [3]:
<resources>
<string name="app_name">Exemple-02</string>
</resources>
O ficheiro [strings.xml] contém as cadeias de caracteres utilizadas pela aplicação. Na linha 2, o nome da aplicação provém da entrada efetuada durante a construção do projeto [4]:
![]() |
- linha 10: uma tag de atividade. Uma aplicação Android pode ter várias atividades;
- linha 12: a atividade é designada como a atividade principal;
- linha 13: e deve aparecer na lista de aplicações que podem ser iniciadas no dispositivo Android.
1.3.3. A atividade principal
![]() | ![]() |
Uma aplicação Android assenta numa ou mais atividades. Aqui, foi gerada uma atividade [1]: [MainActivity]. Uma atividade pode apresentar uma ou mais vistas, 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]. Este será o caso de todas as atividades futuras;
- linha 9: o método [onCreate] é executado quando a atividade é criada. Isto ocorre antes da exibição da vista associada à atividade;
- linha 10: o método [onCreate] da classe pai é chamado. É necessário fazê-lo sempre;
- linha 11: o ficheiro [activity_main.xml] [2] é a vista associada à atividade. A definição XML desta vista é 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 gestor de formatação. O escolhido por predefinição é do tipo [RelativeLayout]. Neste tipo de contentor, os componentes são posicionados uns em relação aos outros (à direita de, à esquerda de, por baixo, por cima);
- linhas m-p: um componente do tipo [TextView] que serve para apresentar texto;
- linha n: o texto apresentado. Não é aconselhável inserir texto fixo nas vistas. É preferível transferir esses textos para o ficheiro [res/values/strings.xml] [3]:
O texto exibido será, portanto, [Hello World!]. Onde será exibido? O contentor [RelativeLayout] irá ocupar todo o ecrã. O [TextView], que é o seu único elemento, será exibido no canto superior esquerdo desse contentor, ou seja, no canto superior esquerdo do ecrã;
O que significa [R.layout.activity_main] na linha 11? A cada recurso Android (vistas, fragmentos, componentes, etc.) é atribuído um identificador. Assim, uma vista [V.xml] que se encontre 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 vista [res / layout / activity_main.xml];
- linha 7: o atributo [R.string.app_name] é o identificador da cadeia [app_name] no ficheiro [res / values / string.xml]:
- linha 19: o atributo [R.mipmap.ic_launcher] é o identificador da imagem [res / mipmap / ic_launcher];
É importante ter em conta que, quando se faz referência a [R.layout.activity_main] no código, está-se a fazer referência a um atributo da classe [R]. O IDE ajuda-nos a identificar os diferentes elementos desta classe:
![]() | ![]() |
1.3.4. Execução da aplicação
Para executar uma aplicação Android, é necessário criar uma configuração de execução:
![]() | ![]() | ![]() |
- em [1], selecionar [Edit Configurations];
- o projeto foi criado com uma configuração [app], que vamos eliminar ([2]) para o recriar;
- em [3], criar uma nova configuração de execução;
![]() |
- em [4], selecionar [Android Application];

- em [5], na lista suspensa, selecione o módulo [app];
- em [6-8], manter os valores propostos por predefinição;
- em [7], a atividade por predefinição é a definida no ficheiro [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 da aplicação (emulador, tablet);
- em [9], indique que esta escolha deve ser guardada;
- confirme a configuração;
![]() |
- em [11], inicie o gestor de emuladores [Genymotion] (ver 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] é apresentado o formulário de seleção do dispositivo de execução. Aqui, está disponível apenas um: o emulador [Genymotion] iniciado anteriormente;
Após alguns instantes, o emulador de software apresenta a seguinte vista:

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 apresenta a lista desses métodos:
![]() |
- [1]: o método [onCreate] é chamado no início da atividade. É neste método que se associa a atividade a uma vista e se recuperam as referências dos seus componentes;
- [2-3]: os métodos [onStart, onResume] são, em seguida, chamados. Verifica-se que o método [onResume] é o último método a ser executado antes de se chegar ao estado [4] da atividade em execução;
1.4. Exemplo-03: reescrita do projeto [Exemple-02] com a biblioteca [Android Annotations]
Vamos agora introduzir a biblioteca [Android Annotations], que facilita a criação de aplicações Android. Para tal, duplicamos o exemplo [Exemple-02] em [Exemple-03], seguindo o procedimento [1-16].
![]() | ![]() |
- em [1], utilize a perspetiva [Project] para visualizar o projeto Android na íntegra;
![]() | ![]() |
![]() | ![]() |
![]() | ![]() | ![]() | ![]() |
Nota: entre [14] e [15], passou-se de uma perspetiva [Android] para uma perspetiva [Project] (ver parágrafo 1.2.2.1).
Em seguida, alteramos o ficheiro [res / values / strings.xml] [17]:
![]() |
O ficheiro [strings.xml] é alterado da seguinte forma:
<resources>
<string name="app_name">Exemple-03</string>
</resources>
Agora, executamos a nova aplicação, que incorporou toda a configuração do ficheiro [Exemple-02]:
![]() | ![]() |
No [19], obtemos o mesmo resultado que com o [Exemple-02], mas com um novo nome.
Vamos agora introduzir a biblioteca [Android Annotations], à qual, por conveniência, chamaremos AA. Esta biblioteca introduz novas classes para anotar os códigos-fonte do Android. Estas anotações serão utilizadas por um processador que irá criar novas classes Java no módulo, classes que participarão na compilação do mesmo, tal como as classes escritas pelo programador. Temos, assim, a seguinte cadeia de compilação:
![]() |
Em primeiro lugar, vamos incluir no ficheiro [build.gradle] as dependências do compilador de anotações AA (processador acima referido):
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 constituem a biblioteca AA;
O ficheiro [build.gradle] é novamente alterado para utilizar um plugin denominado [android-apt], que divide o processo de compilação em duas etapas:
- processamento das anotações do Android, o que dá origem a novas classes;
- compilação de todas as classes do projeto;
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Desde a versão 0.11 do plugin Gradle para Android, é necessário utilizar o android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
- linha 8: versão do plugin [android-apt] que será procurada no repositório central do Maven (linha 3);
- linha 13: ativação deste plugin;
Nesta fase, verifique se a configuração de execução [app] continua a funcionar.
Vamos agora introduzir uma primeira anotação da biblioteca AA na classe [MainActivity]:
![]() |
A classe [MainActivity] é, por enquanto, 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);
}
}
Já explicámos este código no parágrafo 1.3.3. Modificamo-lo da seguinte forma:
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). O seu parâmetro é a vista associada à atividade;
Esta anotação irá produzir uma classe [MainActivity_] derivada da classe [MainActivity] e é esta classe que constituirá a verdadeira atividade. Temos, portanto, de alterar 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 isto, 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 utilizando o AndroidAnnotations 4.0.0.
//
// Pode criar uma obra de maior dimensão que contenha este ficheiro e distribuir essa obra nos termos que escolher.
//
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 iremos tentar explicar o código das classes geradas pela AA. Estas classes gerem a complexidade que as anotações procuram ocultar. No entanto, por vezes, pode ser útil examiná-lo quando se pretende compreender como são «traduzidas» as anotações que utilizamos.
Já podemos executar novamente a configuração [app]. Obtemos o mesmo resultado que anteriormente. Vamos agora partir deste projeto, que iremos duplicar para apresentar os conceitos importantes da programação Android.
1.5. Exemplo-04: vistas 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] para [Exemple-04] (depois de ter eliminado a pasta [app / build] de [Exemple-03]);
- carregamos o projeto [Exemple-04];
- alteremos o nome do projeto no ficheiro [app / res / values / strings.xml] (perspetiva Android);
- eliminemos o ficheiro [Exemple-04 / Exemple-04.iml] (perspetiva Project);
- compilemos e, em seguida, executemos o projeto;
![]() | ![]() |
1.5.2. Criar uma vista
Vamos agora modificar, com o editor gráfico, a vista apresentada pelo projeto [Exemple-04]:
![]() | ![]() |
- no [1-4], crie uma nova vista XML;
- em [5], atribua um nome à vista;
- em [6], indique a baliza raiz da vista. Aqui, escolhemos um contentor [RelativeLayout]. Neste contentor de componentes, estes são posicionados uns em relação aos outros: «à direita de», «à esquerda de», «por baixo de», «por cima de»;
![]() |
O ficheiro [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 contentor [RelativeLayout] vazio que ocupará toda a largura do tablet (linha 3) e toda a sua altura (linha 4);
![]() | ![]() |
- em [1], selecione o separador [Design] na vista [vue1.xml] apresentada;
- em [2-4], ative o modo tablet;
![]() | ![]() | ![]() |
- em [5], defina a escala do tablet para 1;
- em [6], selecione o modo «paisagem» para o tablet;
- a captura de ecrã [7] resume as opções selecionadas.
![]() | ![]() | ![]() |
- em [1], selecione um [Large Text] e arraste-o para a vista [2];
- em [3], clicar duas vezes no componente;
- em [4], altere o texto apresentado. Em vez de o definir «fixamente» na vista XML, vamos externalizá-lo para o ficheiro [res / values / string.xml]
![]() | ![]() | ![]() |
- no [5], adiciona-se um novo valor ao ficheiro [strings.xml];
- no ficheiro [8], atribui-se um identificador à cadeia;
- em [9], é indicado 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 o seu identificador [11];
- para [12]; nas propriedades do componente, altera-se o tamanho dos caracteres [50sp];
- para [13], a nova vista;
O ficheiro [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 efetuadas na interface gráfica encontram-se nas linhas 10, 11 e 14. Os restantes atributos do [TextView] são valores por predefinição ou resultam do posicionamento do componente na vista;
- linhas 7-8: o tamanho do componente corresponde ao do texto que contém (wrap_content) em altura e largura;
- linha 13: a parte superior do componente está alinhada com a parte superior da vista (linha 13), 50 píxeis abaixo (linha 13);
- linha 12: o lado esquerdo do componente está alinhado com a esquerda da vista (linha 13), 213 píxeis à 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 vista [1]:
![]() |
Os componentes são os seguintes:
Posicionar os componentes uns em relação aos outros pode revelar-se uma tarefa frustrante, uma vez que as reações do editor gráfico podem, por vezes, ser surpreendentes. Pode ser preferível utilizar as propriedades dos componentes:
O componente [textView1] deve ser posicionado 50 píxeis abaixo do título e a 50 píxeis da margem esquerda do contentor:
![]() | ![]() | ![]() |
- 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 píxeis do [3] (top);
- em [2], a borda esquerda (left) do componente está alinhada em relação à borda esquerda do contentor a uma distância de 50 píxeis de [3] (left);
O componente [editTextNom] deve ser colocado 60 píxeis à direita do componente [textView1] e alinhado pela parte inferior com esse mesmo componente;
![]() | ![]() |
- no [1], a margem esquerda (left) do componente está alinhada em relação à margem direita (right) do componente [textView1] a uma distância de 60 píxeis do [2] (left). Está alinhado com a borda inferior (bottom:bottom) do componente [textView1] [1];
O componente [buttonValider] deve ser colocado 60 píxeis à direita do componente [editTextNom] e alinhado pela parte inferior com esse mesmo componente;
![]() | ![]() |
- no [1], a margem esquerda (left) do componente está alinhada com a margem direita (right) do componente [editTextNom] a uma distância de 60 píxeis do [2] (left). Está alinhado com a borda inferior do componente (bottom:bottom) [editTextNom] [1];
O componente [buttonVue2] deve ser colocado 50 píxeis abaixo do componente [textView1] e alinhado à esquerda com esse mesmo componente;
![]() | ![]() |
- no [1], a margem esquerda (left) do componente está alinhada em relação à margem esquerda (left) do componente [textView1] e está posicionada abaixo (top:bottom) a uma distância de 50 píxeis do [2] (top);
O ficheiro 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 encontra-se tudo o que foi feito graficamente. Outra forma de criar uma vista é, então, escrever diretamente este ficheiro. Quando se está habituado, isto pode ser mais rápido do que utilizar o editor gráfico.
- Na linha 38, encontra-se uma informação que não mostrámos. Esta é fornecida através das propriedades do componente [editTextNom] [1]:
![]() | ![]() |
Todos os textos provêm do seguinte ficheiro [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 esta vista seja apresentada ao iniciar a aplicação:
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: é a vista [vue1.xml] que passa a ser apresentada pela atividade;
Altere o ficheiro [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_"
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: esta linha de configuração impede que o teclado apareça assim que a vista [vue1] é apresentada. Com efeito, esta vista possui um campo de introdução de dados que recebe o foco quando a vista é apresentada. Este foco faz com que o teclado virtual apareça por predefinição;
Execute a aplicação e verifique se é efetivamente a vista [vue1.xml] que está a ser apresentada:

1.5.3. Gestão de eventos
Vamos agora gerir o clique no botão [Valider] da vista [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");
}
// gestor de eventos
@Click(R.id.buttonValider)
protected void doValider() {
// é apresentado o nome introduzido
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 âmbito [private]. O campo identificado por [R.id.editTextNom] provém da vista [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"/>
Nota: não utilize caracteres acentuados nos identificadores [id]. O AA não os processa corretamente.
- linha 32: a anotação [@Click(R.id.buttonValider)] designa o método que gere o evento «Click» no botão com o identificador [R.id.buttonValider]. Este identificador também provém da vista [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: apresenta o nome introduzido:
- Toast.makeText(...).show(): apresenta um texto no ecrã,
- o primeiro parâmetro de makeText é a atividade,
- o segundo parâmetro é o texto a apresentar 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 executar quando todos os campos anotados por [@ViewById] estiverem inicializados. É importante saber quando esses campos são inicializados. Por exemplo, será que no método [onCreate] é possível utilizar a referência da linha 18? Para responder a esta questão, colocámos registos;
Execute o projeto [Exemple-04] e verifique se algo acontece quando clicar no botão [Valider]. Obtemos os seguintes registos:
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 é encorajado a inserir este tipo de registos nos métodos que gerem o ciclo de vida da aplicação.
1.6. Exemplo-05: navegação entre vistas
No projeto anterior, o botão [Vue n° 2] não foi utilizado. Propõe-se utilizá-lo criando uma segunda vista e mostrando como navegar de uma vista para outra. Existem várias formas de resolver este problema. A que aqui se propõe consiste em associar cada vista a uma atividade. Outro método consiste em ter uma única atividade do tipo [AppCompatActivity] que exiba vistas do tipo [Fragment]. Este será o método utilizado em aplicações futuras.
1.6.1. Criação do projeto
Duplicamos o projeto [Exemple-04] em [Exemple-05]. Para tal, seguiremos o procedimento descrito para duplicar o [Exemple-02] para o [Exemple-03] no parágrafo 1.4 e que foi reproduzido no parágrafo 1.5.
![]() | ![]() |
1.6.2. Adicionar uma segunda atividade
Para gerir uma segunda vista, vamos criar uma segunda atividade. Será esta que irá gerir a vista n.º 2. Estamos aqui num modelo em que uma vista = 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 vista (vue2.xml) associada à nova atividade;
![]() |
- em [7-8], os ficheiros 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 à vista [vue2.xml];
A vista [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 vista, por enquanto vazia, com um gestor de disposição do tipo [RelativeLayout] (linha 2). Na linha 11, verifica-se que 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, foi registada uma segunda atividade.
1.6.3. Navegação da vista n.º 1 para a vista n.º 2
Voltemos ao código da classe [MainActivity], que apresenta a vista n.º 1. A transição para a vista n.º 2 não está, de momento, implementada:
![]() |
Gerimos-a da seguinte forma:
// navegar para a vista n.º 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
// navega-se para a vista n.º 2, passando-lhe o nome introduzido na vista n.º 1
// cria-se um Intent
Intent intent = new Intent();
// associa-se este Intent a uma atividade
intent.setClass(this, SecondActivity.class);
// associamos informações a este 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] gere o «clique» no botão identificado por [R.id.buttonVue2], definido na vista [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 os passos a seguir para a mudança de vista:
- linha 6: criar um objeto do tipo [Intent]. Este objeto permitirá especificar tanto a atividade a iniciar como as informações a passar para a mesma;
- linha 8: associar o Intent a uma atividade, neste caso uma atividade do tipo [SecondActivity], que será responsável por apresentar a vista n.º 2. É importante lembrar que a atividade [MainActivity] apresenta a vista n.º 1. Portanto, temos uma vista = uma atividade. Teremos de definir o tipo [SecondActivity];
- linha 10: opcionalmente, inserir informações no objeto [Intent]. Estas destinam-se à atividade [SecondActivity] que será iniciada. Os parâmetros de [Intent.putExtra] são (Objeto-chave, Objeto-valor). Note-se que o método [EditText.getText()], que devolve o texto introduzido na zona de entrada, não devolve um tipo [String], mas sim um tipo [Editable]. É necessário utilizar o método [toString] para obter o texto introduzido;
- linha 12: inicie a atividade definida pelo objeto [Intent].
Execute o projeto [Exemple-05] e verifique se obtém efetivamente a vista n.º 2 (vazia por enquanto):
![]() | ![]() |
1.6.4. Construção da vista n.º 2
![]() | ![]() |
- No [1-2], eliminamos a vista [main.xml], que já não nos é útil, e, em seguida, alteramos a vista [vue2.xml] da seguinte forma:
![]() |
Os componentes são os seguintes:
O ficheiro 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 obtém efetivamente a nova vista ao clicar no botão [Vue n° 2].
1.6.5. A atividade [SecondActivity]
No [MainActivity], escrevemos o seguinte código:
// navegar para a vista n.º 2
protected void navigateToView2() {
// navega-se para a vista n.º 2, passando-lhe o nome introduzido na vista n.º 1
// cria-se um Intent
Intent intent = new Intent();
// associa-se este Intent a uma atividade
intent.setClass(this, SecondActivity.class);
// associamos informações a este 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. Vamos utilizá-las agora, e isso ocorre no código de [SecondActivity]:
![]() |
O código de [SecondActivity] evolui da seguinte forma:
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) {
// recuperamos o nome
String nom = extras.getString("NOM");
if (nom != null) {
// exibe-se
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
}
}
}
- linha 11: utiliza-se a anotação [@EActivity] para indicar que a classe [SecondActivity] é uma atividade associada à vista [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)]. Neste caso, AA pressupõe que o identificador do componente é idêntico ao campo anotado, neste caso o campo [textViewBonjour];
- linha 23: a anotação [@AfterViews] identifica um método que deve ser executado após os campos anotados por [@ViewById] terem sido inicializados. No método [OnCreate] (linha 19), não é possível utilizar estes campos, uma vez que ainda não foram inicializados. No projeto [Exemple-05], passa-se de uma atividade para outra e, à primeira vista, não era 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 demonstraram que a segunda hipótese se confirmou;
- linha 26: a classe [AppCompatActivity] possui um método [getIntent] que devolve o objeto [Intent] associado à atividade;
- linha 28: o método [Intent.getExtras] devolve 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 contido no objeto [Intent] da atividade;
- linha 34: este é apresentado.
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 é esta classe que constituirá a verdadeira atividade. Isto leva-nos a efetuar alterações em:
[MainActivity]
// navegar para a vista n.º 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
..
// associa-se este 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, deve substituir-se [SecondActivity] por [SecondActivity_];
Teste esta nova versão. Digite um nome na vista n.º 1 e verifique se a vista n.º 2 o apresenta corretamente.
![]() | ![]() |
1.6.6. Navegação da vista n.º 2 para a vista n.º 1
Para navegar da vista n.º 2 para a vista n.º 1, vamos seguir o procedimento visto anteriormente:
- inserir o código de navegação na atividade [SecondActivity], que apresenta a vista n.º 2;
- escrever o método [@AfterViews] na atividade [MainActivity], que apresenta a vista n.º 1;
O código de [SecondActivity] passa a ser o seguinte:
@Click(R.id.buttonVue1)
protected void navigateToView1() {
// cria-se 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: associa-se à atividade [MainActivity_];
- linha 7: recupera-se o Intent associado a [SecondActivity];
- linha 9: recuperam-se as informações deste 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], é adicionado 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);
}
}
}
}
Efetue estas alterações e teste a sua aplicação. Agora, ao regressar da vista n.º 2 para a vista n.º 1, deve encontrar o nome introduzido inicialmente, o que não acontecia até agora.
![]() | ![]() |
1.6.7. Ciclo de vida das atividades
No parágrafo 1.3.5, apresentámos o ciclo de vida de uma atividade. Temos aqui duas atividades e alternamos entre elas durante a execução. Estas atividades contêm dois métodos cuja ordem de chamada um em relação ao outro não é muito clara: [onCreate] e [afterViews]. É importante saber isso. Para tal, adicionamos registos 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 a aplicação, obtemos os seguintes registos:
Os métodos [onCreate, afterViews] da primeira atividade foram executados nesta ordem. Ao clicar no botão [Vue n° 2], os novos registos são os seguintes:
Os métodos [onCreate, afterViews] da segunda atividade foram executados nesta ordem. Ao clicar no botão [Vue n° 1], os novos registos são os seguintes:
A classe [MainActivity] é, portanto, instanciada novamente. Ao clicar no botão [Vue n° 2], os novos registos são os seguintes:
A classe [SecondActivity] é, portanto, instanciada novamente.
As duas atividades são, portanto, sistematicamente recriadas sempre que se muda de atividade.
Vamos agora descobrir uma arquitetura com uma única atividade capaz de gerir várias vistas, denominadas fragmentos. A atividade e as vistas 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 separadores
Vamos aqui explorar as interfaces com separadores. O exemplo é complexo, mas apresenta todos os elementos que iremos utilizar posteriormente: atividade única, gestor de fragmentos (vistas), contentor de fragmentos, navegação entre fragmentos. O conceito de separadores é diferente do de fragmentos e tem um papel secundário no que pretendemos demonstrar neste exemplo.
1.7.1. Criação do projeto
Criamos um novo projeto:
![]() | ![]() |
![]() |
![]() |
- no [7], selecionamos uma atividade com separadores (Tabbed Activity);
![]() |
- em [10-14], mantêm-se os valores propostos por predefinição;
- em [15], seleciona-se separadores com uma barra de títulos;
O projeto criado é então o seguinte:
![]() | ![]() |
- no [1], a atividade;
- em [2], as vistas;
Foi criada automaticamente uma configuração de execução [app], com o nome do módulo, [2b]:
![]() |
É possível executá-la. É então apresentada uma janela com três separadores [3-6]:

1.7.2. Configuração do Gradle
O projeto [Exemple-06] foi gerado com o seguinte ficheiro [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. Esta biblioteca é necessária para os novos componentes utilizados pela aplicação gerada.
1.7.3. A vista [activity_main]
![]() |
A vista [activity_main] é a vista associada à atividade [MainActivity] do projeto. No modo [design], a vista é a seguinte:

Contém os seguintes componentes:
![]() |
- [main_content] corresponde à totalidade da vista;
- [appbar] (requerido a vermelho, 1) é a barra de aplicações. Contém dois componentes:
- [toolbar] (caixa amarela 4) é a barra de ferramentas;
- [tabs] (caixa laranja 5) é a barra de título dos separadores;
- [container] (caixa verde, 2) pode acolher vários fragmentos. Um fragmento é uma vista. Assim, a mesma atividade poderá apresentar várias vistas (fragmentos) neste contentor;
- [fab] (componente 3) é designado por 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 constitui a totalidade da vista. Vemos que se trata de um layout (gestor de disposição de componentes) do tipo [CoordinatorLayout] (linha 2);
- linhas 11-33: o contentor [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 contentor [tabs] (linha 29). Trata-se de um layout do tipo [TabLayout] (linha 28). Este irá apresentar os títulos dos separadores;
- linhas 35-39: o componente [container] (linha 36). É este contentor que apresenta as diferentes vistas da atividade;
- linhas 41-47: o componente [fab] (linha 42) do tipo [FloatingActionButton] (linha 41). Trata-se de um botão no qual se pode clicar. Por predefinição, situa-se no canto inferior direito da vista total;
Não vamos tentar compreender o significado de todos os atributos destes componentes. Vamos utilizá-los tal como estão. É com a experiência e, muitas vezes, no modo [design] que se descobre a sua função. Neste modo, verifica-se que os componentes têm várias dezenas de atributos. Em geral, apenas alguns são inicializados, mantendo os restantes um valor por predefinição.
No entanto, vamos esclarecer alguns pontos. A maioria dos valores que configuram as diferentes vistas está reunida na pasta [res / values]:
![]() |
Estes valores são referenciados nas linhas 15-16, 23, 39 e 46 do ficheiro [activity_main.xml]. Vejamos um exemplo:
- linha 15:
android:paddingTop="@dimen/appbar_padding_top"
A anotação [@dimen] remete para o ficheiro [res / values / dimens.xml]:
<resources>
<!-- Margens de ecrã predefinidas, 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 ficheiro [activity_main.xml] faz referência à linha (f) acima;
De forma análoga, a anotação:
- [@string] refere-se ao ficheiro de recursos [res / values / strings.xml];
- [@color] refere-se ao ficheiro de recursos [res / values / colors.xml];
- [@style] refere-se ao ficheiro 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. A gestão de fragmentos e separadores
O código de [MainActivity] relativo aos fragmentos e separadores é 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 gestor de fragmentos
private SectionsPagerAdapter mSectionsPagerAdapter;
// o contentor de fragmentos
private ViewPager mViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
// pai
super.onCreate(savedInstanceState);
// vista
setContentView(R.layout.activity_main);
// barra de ferramentas
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// o gestor de fragmentos
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// o contentor de fragmentos está associado ao gestor de fragmentos
// ou seja, o fragmento n.º i do contentor de fragmentos é o fragmento n.º i fornecido pelo gestor de fragmentos
mViewPager = (ViewPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// a barra de separadores também está associada ao contentor de fragmentos
// ou seja, o separador n.º i apresenta o fragmento n.º i do contentor
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
}
// um fragmento
public static class PlaceholderFragment extends Fragment {
...
}
// o gestor de fragmentos
// é a ele que se solicitam os fragmentos a apresentar na vista principal
// deve definir os métodos [getItem] e [getCount] — os restantes são opcionais
public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
}
}
- linha 28: o Android fornece um contentor de vistas do tipo [android.support.v4.view.ViewPager] (linha 12). É necessário fornecer a este contentor um gestor de vistas ou fragmentos. É o programador que o fornece;
- linha 25: o gestor de fragmentos utilizado neste exemplo. A sua implementação encontra-se nas linhas 61-63;
- linha 31: o método executado aquando da criação da atividade;
- linha 35: a vista [activity_main.xml] está associada à atividade;
- linha 37: recupera-se a referência do componente [toolbar] da vista através do seu identificador;
- linha 38: esta barra de ferramentas torna-se a barra de ação (um conceito do Android) da atividade;
- linha 40: o gestor 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], recupera-se a referência do contentor de fragmentos através do seu identificador;
- linha 45: o gestor de fragmentos é associado ao contentor de fragmentos. Isto significa que, quando for solicitado ao contentor de fragmentos que exiba o fragmento n.º i, este será solicitado ao gestor de fragmentos;
- linha 48: recupera-se uma referência à barra de separadores através do seu identificador;
- linha 49: o gestor de separadores está associado ao contentor de fragmentos. Isto significa que, quando se clicar no separador n.º i, o contentor exibirá o fragmento n.º i. A associação estabelecida entre o gestor de separadores e o contentor de fragmentos evita-nos qualquer gestão dos separadores. Assim, não temos de definir um gestor de eventos para o clique num separador. A associação com o contentor de fragmentos fornece-o por predefinição. Veremos um exemplo em que haverá mais fragmentos do que separadores. Nesse caso, não se faz essa associação.
O gestor de fragmentos [SectionsPagerAdapter] é o seguinte:
// o gestor de fragmentos
// é a ele que se solicitam os fragmentos a apresentar na vista principal
// deve definir os métodos [getItem] e [getCount] — os restantes 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 apresenta-se
return PlaceholderFragment.newInstance(position + 1);
}
// indica o número de fragmentos geridos
@Override
public int getCount() {
return 3;
}
// opcional — atribui um título aos fragmentos geridos
@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 uma aplicação dependem desta. O gestor de fragmentos é definido pelo programador;
- linha 5: o gestor de fragmentos estende a classe Android [android.support.v4.app.FragmentPagerAdapter]. O construtor é-nos imposto. Temos de definir, pelo menos, os dois métodos seguintes:
- int getCount(): devolve o número de fragmentos a gerir;
- Fragment getItem(i): devolve o fragmento n.º i;
O método CharSequence getPageTitle(i), que apresenta o título do fragmento n.º i, é opcional. Como o gestor de separadores foi associado ao gestor de fragmentos, o título do separador n.º i será o título do fragmento n.º i. Assim, os títulos das linhas 27-33 serão os títulos dos separadores;
- linhas 18-21: getCount indica o número de fragmentos geridos, neste caso três;
- linhas 11-15: getItem(i) devolve o fragmento n.º i. Aqui, todos os fragmentos serão idênticos, do tipo [PlaceholderFragment];
- linhas 24-35: getPageTitle(int i) devolve o título do fragmento n.º i;
1.7.4.2. Os fragmentos apresentados
![]() |
Os fragmentos da atividade têm aqui todos o mesmo tipo e estão todos associados à seguinte vista 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 vista (fragmento): um [TextView] identificado por [section_label];
No [MainActivity], os fragmentos geridos são do tipo [PlaceholderFragment], a saber:
// 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 vista [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);
// o seu conteúdo é alterado
textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
// a vista é devolvida
return rootView;
}
}
- linha 2: a classe [PlaceholderFragment] estende a classe Android [Fragment]. Em geral, é sempre assim;
- linha 2: a classe [PlaceholderFragment] é estática. O seu método [newInstance] (linha 10) permite obter instâncias do tipo [PlaceholderFragment];
- linhas 10-19: o método [newInstance] cria e devolve 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. Este método deve devolver a vista associada ao fragmento.
- linha 25: a vista [fragment_main.xml] está associada ao fragmento;
- linha 27: esta vista contém um componente [TextView], cuja referência é obtida através do seu identificador;
- linha 29: é exibido 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 ficheiro [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 atribuir 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) irá, portanto, devolver o valor [sectionNumber] das linhas (g) e (b) acima;
- linha 31: apresenta-se a vista assim criada;
1.7.4.3. Gestão do menu
Na aplicação gerada, existe um menu:
![]() |
O conteúdo do ficheiro [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 elemento do menu identificado por [action_settings] (linha 5);
- linha 6: o rótulo da opção do menu. Encontra-se no ficheiro [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 encontra-se no canto superior direito da janela de execução do Android):
![]() | ![]() |
Este menu é gerido da seguinte forma na atividade [MainActivity]:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Expande o menu; isto adiciona itens à barra de ações, caso esta 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á
// tratar automaticamente os cliques no botão Início/Para cima, desde que
// que especifique 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: este método é chamado quando o sistema está pronto para criar o menu da aplicação. O parâmetro de entrada [Menu menu] é um menu vazio que ainda não tem opções;
- linha 4: o ficheiro [res / menu / menu_main.xml] é utilizado. Ao objeto [Menu menu], passado como parâmetro, são atribuídas as opções de menu definidas nesse ficheiro;
- linha 5: indica-se que o menu foi criado;
- 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 tratado (linha 17);
- linha 20: o evento é passado para a classe pai;
Para compreender melhor o que acontece com este menu, adicionamos registos ao código anterior:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("menu", "création menu en cours");
// Inflacionar o menu; isto adiciona itens à barra de ação, caso esta 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á
// tratar automaticamente os cliques no botão Início/Para cima, desde que
// que especifique 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;
}
// atividade-pai
return super.onOptionsItemSelected(item);
}
1.7.4.4. O botão flutuante
A vista gerada tem um botão flutuante:
![]() |
Este componente está definido na vista 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 Android, a de um envelope.
Este componente é gerido na classe [MainActivity] da seguinte forma:
// 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 vista associada à atividade (activity_main);
- linhas 3-9: associa-se-lhe um manipulador para gerir o clique no mesmo;
- linha 6: a classe [Snackbar] permite apresentar mensagens temporárias na vista através do seu método [Snackbar.make]. O primeiro argumento é uma vista a partir da qual o método [Snackbar] irá procurar uma vista pai na qual exibir a mensagem. Aqui, [view] é a vista do envelope em que se clicou (linha 5). A vista pai que será encontrada será a vista [activity_main]. O segundo argumento é a mensagem a exibir. O terceiro argumento é a duração da exibição (SHORT ou LONG);
- linha 7: é possível clicar na mensagem apresentada e, assim, desencadear uma ação. Aqui, não está associada nenhuma ação ao clique na mensagem. Por fim, o método [show] apresenta a mensagem;
Ao clicar no botão flutuante, obtém-se o seguinte resultado visual:
![]() |
1.7.5. Execução do projeto
Agora que explicámos os detalhes do código gerado, podemos compreender melhor a sua execução:

Quando se clica no separador n.º i, o fragmento n.º i é apresentado no contentor de vistas. Isto é visível no texto apresentado em [4]. É também possível notar que se pode alternar entre separadores arrastando a vista para a direita ou para a esquerda com o rato (deslizar). Veremos que é possível controlar este comportamento.
Ao clicar na opção do menu em [6], obtêm-se os seguintes registos:
![]() |
1.7.6. Ciclo de vida dos fragmentos
![]() | ![]() |
- em [1], verifica-se que o método [onCreateView] e os seguintes são executados na primeira exibição do fragmento e sempre que a atividade tiver de o exibir novamente;
Para acompanhar o ciclo de vida da atividade e dos fragmentos, adicionamos os seguintes registos 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 novamente o projeto. Os primeiros registos são os seguintes:
- linha 1: criação da atividade;
- linha 2: execução do seu 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 recordar aqui o código responsável pela criação dos fragmentos:
// o gestor de fragmentos
// é a ele que se solicitam os fragmentos a exibir na vista principal
// deve definir os métodos [getItem] e [getCount] — os restantes 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 apresenta-se
return PlaceholderFragment.newInstance(position + 1);
}
...
- linhas 11-15: um fragmento é instanciado por [newInstance] sempre que o contentor de fragmentos o solicitar;
Os registos acima mostram que os dois primeiros fragmentos foram instanciados e inicializados.
Agora, cliquemos no separador n.º 2. Os novos registos são os seguintes:
- linhas 1-3: o fragmento n.º 3 é instanciado e inicializado. Recorde-se que é o fragmento n.º 2 que está a ser apresentado;
Agora, vamos clicar no separador n.º 3. Aqui não há nenhum registo. Provavelmente porque o fragmento n.º 3 a exibir já tinha sido instanciado. Agora, voltemos ao separador n.º 1. Os registos são então os seguintes:
O fragmento n.º 1 não é instanciado novamente, mas o seu método [onCreateView] é executado novamente. Este comportamento repete-se para os outros dois fragmentos.
A partir destes registos, 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 é importante saber e o que os registos confirmam é que, por predefinição, quando um fragmento n.º i é apresentado, os fragmentos i-1 e i+1 são instanciados, caso ainda não o tenham sido. É isso que explica, por exemplo, que, no arranque, quando é necessário apresentar o fragmento n.º 1, são os fragmentos 1 e 2 que foram instanciados e inicializados. O que os registos também mostram é que o método [getItem(i)] só é chamado uma vez, mesmo que o fragmento n.º i seja apresentado várias vezes. Assim, parece que o contentor de fragmentos [ViewPager], que deve apresentar o fragmento n.º i, solicita este uma única vez ao gestor de fragmentos [SectionsPagerAdapter]. Posteriormente, não o volta a solicitar e continua a utilizar aquele que obteve.
Por fim, os registos fornecem indicações sobre o método [onCreateView] dos fragmentos:
- no arranque, os fragmentos 1 e 2 foram instanciados e o 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. Por conseguinte, não é possível utilizá-lo para atualizar o fragmento 2. No entanto, o utilizador pode, com o fragmento 1, ter realizado uma operação cujo resultado deveria ser apresentado pelo fragmento 2. Verifica-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 introduzir neste último as anotações do Android. Para tal, siga o procedimento descrito no parágrafo 1.4. Obtemos o seguinte resultado:
![]() | ![]() |
1.8.2. Configuração do Gradle
![]() |
Alteramos o ficheiro [build.gradle] da seguinte forma:
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Desde a versão 0.11 do plugin Gradle do Android, é necessário utilizar o android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
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'
}
Adicionámos 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 ficheiro [MainActivity]:
![]() |
A classe [MainActivity] evolui da seguinte forma:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// o gestor de fragmentos
private SectionsPagerAdapter mSectionsPagerAdapter;
// o contentor de fragmentos
@ViewById(R.id.container)
protected MyPager mViewPager;
// o gestor de separadores
@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 gestor de fragmentos
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// o contentor de fragmentos está associado ao gestor de fragmentos
// ou seja, o fragmento n.º i do contentor de fragmentos é o fragmento n.º i fornecido pelo gestor de fragmentos
mViewPager.setAdapter(mSectionsPagerAdapter);
// a barra de separadores também está associada ao contentor de fragmentos
// ou seja, o separador n.º i apresenta o fragmento n.º i do contentor
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] numa classe gerida por AA. O seu parâmetro [R.layout.activity_main] é o identificador da vista [activity_main.xml] associada à atividade;
- linhas 11-12: o componente identificado por [R.id.tabs] é inserido no campo [tabLayout]. Trata-se do gestor de separadores;
- 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 anteriormente se encontrava no método [onCreate] é transferido para um método com um nome qualquer, mas anotado com [@AfterViews] (linha 23). No método assim anotado, tem-se a garantia de que todos os componentes da interface visual anotados com [@ViewById] foram inicializados;
- Além disso, foram inseridos registos para acompanhar o ciclo de vida da atividade;
Recorde-se que a anotação [@EActivity] irá gerar uma classe [MainActivity_], que será a verdadeira atividade do projeto. Por conseguinte, é necessário alterar o ficheiro [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_"
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.
Nesta altura, execute novamente o projeto e verifique se continua a obter a interface com separadores.
1.8.4. Reescrita dos fragmentos
Vamos rever a gestão dos fragmentos do projeto. Por enquanto, a classe [PlaceholderFragment] é uma classe interna estática da atividade [MainActivity]. Vamos voltar a um caso de utilização mais comum, aquele em que os fragmentos são definidos em classes externas. Além disso, introduzimos 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]. É 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 vista apresentada por um contentor 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 vista XML associada ao fragmento, neste caso a vista [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 vista associada ao fragmento. Pode ser utilizado para atualizar a interface visual que será exibida;
- linha 47: deve-se chamar o método com o mesmo nome da classe pai;
- linha 49: existe uma dificuldade em determinar se o método [onResume] pode ou não ser executado antes da inicialização do campo da linha 20. Os registos criados para acompanhar o ciclo de vida do fragmento irão esclarecer-nos sobre isso. Por enquanto, e por precaução, faz-se um teste de nulidade;
- linha 51: atualizamos a informação do campo [textViewInfo] com o argumento inteiro passado ao fragmento aquando da sua criação;
A classe [MainActivity] perde a sua classe interna [PlaceholderFragment] e o seu gestor de fragmentos evolui 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 ao 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 geridos
@Override
public int getCount() {
return fragments.length;
}
// opcional — atribui um título aos fragmentos geridos
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
}
- linha 4: os fragmentos são colocados numa matriz;
- linhas 16-23: a inicialização da matriz de fragmentos é feita no construtor. São do tipo [PlaceholderFragment_] (linha 18) e não [PlaceholderFragment]. A classe [PlaceholderFragment] foi, de facto, anotada com uma anotação AA e dará origem a uma classe [PlaceholderFragment_] derivada de [PlaceholderFragment], sendo esta a classe que a atividade deve utilizar. A cada fragmento criado é passado um argumento inteiro que será exibido pelo fragmento;
- linhas 42-45: alterámos os títulos dos fragmentos. Como estes são também os títulos dos separadores, deveremos ver uma alteração na barra de separadores;
Vamos compilar [Make] e [1] neste projeto:
![]() | ![]() |
- em [2], vemos que as classes geradas pela biblioteca AA são criadas na pasta [app / build / generated / source / apt / debug] (é necessário estar na perspetiva [Project] para ver [2]);
Execute o projeto [Exemple-07] e verifique se continua a funcionar.
1.8.5. Análise dos registos
Quando se inicia a aplicação, os registos são os seguintes:
- linha 1: construção da única atividade;
- linha 2: método [afterViews] da atividade: os seus campos anotados por [@ViewById] são inicializados;
- linhas 3-5: construção dos três fragmentos;
- linhas 6-7: o contentor 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;
Note-se que temos aqui a resposta a uma questão colocada anteriormente: o método [onResume] do fragmento 1, por exemplo (linha 12), é executado após o método [afterViews] do fragmento (linha 11). Assim, quando o método [onResume] é executado, pode utilizar os campos anotados pelo [@ViewById]. Podemos, portanto, escrever agora o método [onResume] da seguinte forma:
@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, passemos do separador 1 para o separador 2. Os novos registos são os seguintes:
- linha 1: o contentor de fragmentos [ViewPager] solicita o fragmento n.º 3;
- linhas 2-3: métodos do fragmento n.º 3. Recorde-se que este fragmento tinha sido instanciado logo no arranque da aplicação;
- linhas 4-5: o método [onResume] do fragmento n.º 3 é executado. Recorde-se que é o fragmento n.º 2 que está a ser apresentado;
Agora, passemos do separador 2 para o separador 3. Não há qualquer registo. Portanto, nenhum dos métodos [onCreateView, afterViews, onResume] do fragmento n.º 3 é executado. O texto [Hello World from section:3] é apresentado corretamente apenas porque esse texto já tinha sido criado na etapa anterior, durante a apresentação do fragmento n.º 2. Recordemos, de facto, que, nessa etapa, o método [onResume] do fragmento n.º 3 tinha sido executado. Percebe-se aqui que, tal 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 apresentado pelo fragmento, nenhum destes dois métodos o poderia fazer.
Agora, voltemos do separador n.º 3 para o separador n.º 1. Os registos 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 referido, este método é chamado apenas uma vez para cada fragmento;
Agora, passemos do separador 1 para o separador adjacente 2. Temos os seguintes registos:
Surpreendente, não é? Todos os métodos do fragmento n.º 3 são reexecutados.
Para compreender estes fenómenos, é preciso lembrar que, por predefinição, quando o contentor de fragmentos vai apresentar o fragmento i, inicializa os fragmentos i-1, i e i+1. Relemos os registos à luz desta informação.
Em primeiro lugar, os registos no arranque da aplicação:
Como o contentor de fragmentos vai apresentar o fragmento 1, os fragmentos 1 e 2 são inicializados (linhas 8-15).
Passamos agora do separador 1 para o separador 2:
Como o contentor de fragmentos vai apresentar o fragmento 2, os fragmentos 1, 2 e 3 têm de ser inicializados. Os fragmentos 1 e 2 já estão inicializados desde a etapa anterior. O fragmento 3 é inicializado nas linhas 2-5.
Passamos do separador 2 para o separador 3. Não há registos. Como o contentor de fragmentos vai apresentar o fragmento 3, os fragmentos 2 e 3 têm de ser inicializados. No entanto, desde a etapa anterior, já se encontram inicializados. O que não se vê aqui é que o fragmento 1, que não é adjacente ao fragmento 3, perde o seu estado, que não é mantido na memória.
Passamos do separador 3 para o separador 1. Os registos são os seguintes:
Como o contentor de fragmentos vai apresentar o fragmento 1, o fragmento 2 também tem de ser inicializado. Já o está desde a etapa anterior. Nesta mesma etapa, o estado do fragmento 1 tinha sido perdido. Por isso, é reinicializado nas linhas 1-4. O que não se vê aqui é que o fragmento 3, que não é adjacente ao fragmento 1, perde o seu estado, que não é, portanto, mantido na memória.
Ao passar do separador 1 para o separador adjacente 2, obtêm-se os seguintes registos:
Como o contentor de fragmentos vai apresentar o fragmento 2, os fragmentos 1, 2 e 3 têm de ser inicializados. Os fragmentos 1 e 2 já estão inicializados desde a etapa anterior. O fragmento 3 é inicializado nas linhas 1-4.
O que aprendemos?
- que a gestão por predefinição dos fragmentos é muito específica e que é necessário conhecê-la se não quisermos enlouquecer. É possível alterar este modo de gestão e fá-lo-emos um pouco mais adiante;
- que, com esta gestão por predefinição, nenhum dos métodos [onCreateView, onResume] pode ser utilizado para atualizar o fragmento que vai ser apresentado, pois não há garantia de que venham a ser executados;
1.8.6. onDestroyView
O método [onDestroyView] faz parte do ciclo de vida dos fragmentos (ver parágrafo 1.7.6):
![]() | ![]() |
Verifica-se que, no ciclo de vida de um fragmento:
- o método [onCreateView] pode ser executado várias vezes;
- antes de regressar ao método [onCreateView] posteriormente, há necessariamente uma passagem pelo método [onDestroyView] [2];
Vamos inserir estes métodos nos fragmentos para acompanhar melhor o seu ciclo de vida. O código do fragmento passa a ser o seguinte:
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 vista apresentada por um contentor de fragmentos
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
...
@Override
public void onDestroyView() {
// registo
Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// pai
super.onDestroyView();
}
}
Vamos executar a aplicação. Os primeiros registos são os seguintes:
- linha 1: construção da atividade única;
- linha 2: método [afterViews] da atividade: os seus campos anotados por [@ViewById] são inicializados;
- linhas 3-5: construção dos três fragmentos;
- linhas 6-7: o contentor de fragmentos [ViewPager] solicita os dois primeiros fragmentos;
- linhas 8-9: a vista do fragmento 2 é criada (não necessariamente tornada visível);
- linhas 10-11: a vista 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;
Passemos do separador 1 para o separador 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 contentor de fragmentos solicita o terceiro fragmento;
- linhas 2-3: a vista do fragmento 3 é criada (não necessariamente apresentada);
- linhas 4-5: o método [onResume] do fragmento 3 é executado;
- linha 6: o método [onDestroyView] do fragmento 1 é executado. Isto implica que, quando o utilizador regressar ao fragmento 1 ou a um fragmento adjacente, o ciclo de vida desse fragmento será reexecutado;
Voltamos do separador 3 para o separador 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 tinha sido alvo de um [onDestroyView];
- linha 5: agora é o fragmento 3 que vê o seu método [onDestroyView] executado. Mais uma vez, quando o utilizador regressar 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 vista associada ao fragmento, mas não a torna necessariamente visível. É isso que vamos ver agora. O método [Fragment.setUserVisibleHint] é executado sempre que a visibilidade do fragmento muda. Adicionamos este método ao código do fragmento:
package exemples.android;
....
// um fragmento é uma vista apresentada por um contentor 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) {
// registo
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
}
}
No arranque, os registos 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 registos das linhas 7, 9-10 mostram que apenas o fragmento 1 se torna visível. Vemos também que este se torna visível antes da execução do seu método [onCreateView];
Passemos do separador 1 para o separador 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á visível (linha 4);
Passemos do separador 2 para o separador 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á visível (linha 2);
Voltemos ao separador 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á visível (linha 3);
O que aprendemos?
- o método [setUserVisibleHint] é executado uma vez com a propriedade [isVisibleToUser] a true, para o fragmento que vai ser exibido;
- não é possível determinar quando este 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 registos anteriores mostram que, quando o contentor de fragmentos [ViewPager] se prepara para apresentar o fragmento n.º i, executa, caso ainda não o tenha feito, o ciclo de vida dos fragmentos adjacentes i-1 e i+1. Este funcionamento pode ser controlado através do método [ViewPager].setOffscreenPageLimit:
Com a instrução acima,
- quando o contentor de fragmentos [ViewPager] se prepara para apresentar o fragmento n.º i, executa, caso ainda não o tenha 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 repete-se para os fragmentos adjacentes do intervalo [j-n, j+n];
- os fragmentos inicializados na etapa 1 e que já não se encontram na vizinhança [j-n, j+n] do novo fragmento podem, então, ser submetidos a uma operação [onDestroyView]. No entanto, pude observar noutras aplicações, nomeadamente na do capítulo 3, que tal não era sistematicamente o caso;
Alteramos o método [MainActivity.afterViews] da seguinte forma:
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
// barra de ferramentas
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// o gestor de fragmentos
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// o contentor de fragmentos está associado ao gestor de fragmentos
// ou seja, o fragmento n.º i do contentor de fragmentos é o fragmento n.º i fornecido pelo gestor de fragmentos
mViewPager.setAdapter(mSectionsPagerAdapter);
// inibe-se o deslize entre fragmentos
mViewPager.setSwipeEnabled(false);
// deslocamento dos fragmentos
mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);
// a barra de separadores também está associada ao contentor de fragmentos
// ou seja, o separador n.º i apresenta o fragmento n.º i do contentor
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 inicializar como o número total de fragmentos menos 1. Assim, no arranque, quando o contentor de fragmentos exibir o fragmento n.º 1, irá, ao mesmo tempo, inicializar os fragmentos 2, 3, ..., n com n = 1 + mSectionsPagerAdapter.getCount() - 1=mSectionsPagerAdapter.getCount(). São, portanto, todos os fragmentos que serão inicializados. Quando a janela de visualização se deslocar para outro fragmento, o contentor de fragmentos:
- verá que todos os fragmentos adjacentes ao novo fragmento já estão inicializados e, por isso, não os inicializará;
- uma vez que a adjacência do novo fragmento também abrange a totalidade dos fragmentos, nenhum será «desinicializado» pelo contentor de fragmentos;
No total, deveríamos ver todos os fragmentos instanciados e inicializados no arranque da aplicação e, depois, nunca mais. É isso que vamos verificar agora, analisando os registos.
No arranque, temos os seguintes registos:
- linhas 4-6: construção dos três fragmentos;
- linhas 7, 9, 11: o contentor de fragmentos solicita os três fragmentos. Na versão anterior, solicitava dois;
- linhas 14-25: o ciclo de vida dos três fragmentos é executado;
Passemos agora do separador 1 para o separador 2:
Passemos do separador 2 para o separador 3:
Depois, da guia 3 para a guia 1:
Os registos confirmam a teoria. Todos os fragmentos foram instanciados e inicializados no arranque. Posteriormente, os métodos do seu ciclo de vida deixam de ser executados. Temos aqui um funcionamento muito previsível dos fragmentos, o que facilita enormemente a sua utilização.
O que pretendemos descobrir é uma forma de atualizar um fragmento que vai ser apresentado, independentemente da adjacência de fragmentos escolhida pelo programador. Os registos mostraram-nos duas coisas:
- o método [setUserVisibleHint, true] é sempre executado para o fragmento que vai ser apresentado e não para os outros;
- este evento pode ocorrer antes ou depois do ciclo de vida do fragmento. Isso depende da adjacência de fragmentos escolhida pelo programador. Trata-se de 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 registos no arranque da aplicação, 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
- vemos que, quando o fragmento 1 se torna visível, a sua vista ainda não foi criada. Por isso, não é possível alterá-la. Isso poderá então 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 de escrever o método [onCreateView]. Assim, o método [onResume] parece ser o mais adequado neste caso para atualizar o fragmento 1;
Quando passámos do separador 1 para o separador 2, os registos 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 passámos do separador 2 para o separador 3, os registos 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 passámos do separador 3 para o separador 1, os registos 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, é necessário utilizar o método [onResume] do fragmento 1 (linhas 6-7) para atualizar o fragmento 1.
Assim, neste exemplo, vemos que, para atualizar um fragmento que vai ser exibido, dispomos de dois métodos: [setUserVisibleHint] e [onResume].
Vamos implementar esta solução num novo projeto em que cada fragmento deverá exibir o número de vezes que foi exibido, o que designaremos por «visita». Será, portanto, necessário atualizar a sua exibição sempre que for exibido. É precisamente este o problema que procuramos resolver.
Antes disso, vamos analisar a última etapa do ciclo de vida de uma atividade ou de um fragmento: aquela em que é eliminado. O sistema pode tomar a iniciativa de eliminar uma atividade se outras atividades com maior prioridade exigirem recursos indisponíveis. Para libertar esses recursos, o sistema tomará a iniciativa de eliminar certas atividades. O método [onDestroy] da atividade e dos fragmentos será então chamado.
1.8.9. OnDestroy
![]() | ![]() | ![]() |
Vamos permitir que o utilizador elimine a atividade através de uma opção de menu [5]. Para tal, adicionamos uma nova opção de menu no ficheiro [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 desta nova opção é adicionado ao ficheiro [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], trata-se 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ções. A barra de ações irá
// tratar automaticamente os cliques no botão Início/Acima, desde que
// que especifique 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 terminar 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 é concluída por ação de software;
Agora, executemos esta nova versão e, assim que a primeira vista for apresentada, cliquemos na opção de menu [Terminate]. Os registos são então os seguintes:
- 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 do seu método [onDestroy];
- linhas 6-9: esta operação repete-se para os outros dois fragmentos;
Recorde-se, portanto, que o método [onDestroy] da atividade e dos fragmentos é chamado quando a atividade está prestes a ser eliminada pelo sistema, pelo programador ou pelo utilizador. Este método pode ser utilizado para guardar informações, por exemplo, localmente no tablet, de modo a recuperá-las quando o utilizador voltar a iniciar a aplicação.
1.9. Exemplo-08: atualização de um fragmento com uma adjacência variável de fragmentos
1.9.1. Criação do projeto
Duplica-se o projeto [Exemple-07] em [Exemple-08]. Para tal, segue-se o procedimento descrito para duplicar o [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. 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 vista apresentada por um contentor 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;
// registo
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 do fragmento
public void update() {
// o trabalho a realizar depende do n.º da visita
if (numVisit > 1) {
// registo
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 registos
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. Utilizávamo-lo para inicializar o texto do fragmento (linha 42). Continuamos a fazê-lo, mas para que isso aconteça apenas uma vez, gerimos uma variável booleana [initDone] (linha 44) para indicar que a inicialização foi efetuada e que não é necessário repeti-la;
- linhas 56-59: introduzimos o método [onDestroyView] para registar o facto de que, da próxima vez que o fragmento for exibido novamente, o seu ciclo de vida será reexecutado;
- os registos 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 registos revelaram que, pelo menos, um dos dois é executado após o método [@AfterViews]. Os registos nunca revelaram que ambas pudessem ser executadas em simultâneo após o método [@AfterViews]. É uma ou a outra. Por precaução, será definido um valor booleano [updateDone] quando for efetuada uma atualização;
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;
// registo
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 vai ser armazenado em cache
updateDone = false;
}
}
@Override
public void onResume() {
// pai
super.onResume();
// registo
Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// atualização
if (isVisibleToUser && !updateDone) {
update();
updateDone = true;
}
}
- linha 14: guarda-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. Precisamos, de facto, de um evento para reiniciar 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 efetuadas. Aproveitamos o facto de o fragmento já não estar visível para o fazer. Quando voltar a ficar visível, a atualização do fragmento terá de ser feita novamente;
- linhas 32-42: os registos mostram que, dependendo da adjacência escolhida para os fragmentos, o método [onResume] pode ser executado mesmo que o fragmento não esteja visível. Se não estiver visível, não se efetua a atualização (linha 39) e, tal como fizemos para o [setMenuVisibility], gerimos o valor booleano [updateDone].
Por fim, o método [onDestroyView] é o seguinte:
@Override
public void onDestroyView() {
// pai
super.onDestroyView();
// atualização do indicador
afterViewsDone = false;
// registo
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 termina. Um outro ciclo poderá recomeçar posteriormente.
- linha 6: o método [onDestroyView] elimina qualquer ligação com a vista associada ao fragmento. Esta será recriada no próximo ciclo de vida do fragmento. Por enquanto, temos de definir o valor booleano [afterViews] como false, para indicar que a ligação com a vista já não existe;
Vamos executar a aplicação com 5 fragmentos com uma adjacência de 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;
// o gestor de fragmentos
private SectionsPagerAdapter mSectionsPagerAdapter;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
....
// deslocamento dos fragmentos
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
...
}
Os registos no arranque 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 contentor de fragmentos solicita todos os fragmentos adjacentes ao fragmento 1;
- linhas 9, 11, 13: o método [setUserVisibleHint] destes fragmentos é executado com [visibleToUser] a false;
- linha 14: o método [setUserVisibleHint] do fragmento 1 é executado com [visibleToUser] até true;
- linhas 15-17: o método [afterViews] dos 3 segmentos adjacentes é chamado. Vemos, portanto, aqui um caso em que este método é chamado depois de um fragmento se ter tornado visível (o fragmento 1, linha 14);
- linhas 18-20: é chamado o método [onResume] dos 3 segmentos adjacentes;
Passamos do separador 1 para o separador 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
- uma vez que a adjacência dos fragmentos é deslocada uma posição para a direita, o fragmento 4 é solicitado pelo contentor de fragmentos;
- linha 2: o método [setUserVisibleHint] do fragmento 4 é chamado com [visibleToUser] a false;
- linha 3: o método [setUserVisibleHint] do fragmento 1 é chamado com [visibleToUser] até false. Com efeito, o fragmento 1 está agora oculto;
- linha 4: o método [setUserVisibleHint] do fragmento 2 é chamado com [visibleToUser] a true. O fragmento 2 está agora visível;
- linhas 5-6: o ciclo de vida do fragmento 4 continua;
Passa-se do separador 2 para o separador 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
- uma vez que a adjacência dos fragmentos é deslocada uma posição para a direita, o fragmento 5 é solicitado pelo contentor de fragmentos;
- linha 2: o método [setUserVisibleHint] do fragmento 5 é chamado com [visibleToUser] a false;
- linha 3: o método [setUserVisibleHint] do fragmento 2 é chamado com [visibleToUser] a false. Com efeito, o fragmento 2 está agora oculto;
- linha 4: o método [setUserVisibleHint] do fragmento 3 é chamado com [visibleToUser] em true. O fragmento 3 está agora visível;
- linhas 5-6: o ciclo de vida do fragmento 5 continua;
Passamos do separador 3 para o separador 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 está agora oculto;
- linha 2: o fragmento 4 está agora visível. Note-se que não há execução do ciclo de vida do fragmento 4. Este já foi executado duas etapas anteriormente;
- linha 3: o fragmento 1 sai da adjacência do fragmento 4 exibido. O seu método [onDestroyView] é executado. Da próxima vez que for exibido, o seu ciclo de visualização [onCreateView, afterViews, onResume] será executado novamente;
Passamos do separador 4 para o separador 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 está agora oculto;
- linha 2: o fragmento 5 está agora visível. Note-se que não há execução do ciclo de vida do fragmento 5. Este já foi executado duas etapas anteriormente;
- linha 3: o fragmento 2 sai da adjacência do fragmento 5 exibido. O seu método [onDestroyView] é executado;
Passamos do separador 5 para o separador 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. Com efeito, este tinha perdido a ligação com a sua vista;
- linhas 2, 5, 8, 9: pela mesma razão, 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 registos nunca mostraram que os métodos [setUserVisibleHint] e [onResume] tentassem ambos atualizar o fragmento. É um ou outro. Convidamos o leitor a realizar outros testes e a acompanhar os registos para compreender bem o conceito de adjacência e o ciclo de vida dos fragmentos.
Agora, vamos assumir 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 registos no arranque 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 registos mostram que o ciclo de vida dos 5 fragmentos é executado;
- o fragmento 1 é apresentado na linha 18;
Passamos do separador 1 para o separador 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 do separador 2 para o separador 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 do separador 3 para o separador 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 do separador 4 para o separador 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 está visível;
Passamos do separador 5 para o separador 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 do separador 1 para o separador 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;
Notamos que, com a adjacência total, o comportamento dos fragmentos é muito mais previsível.
Agora, vamos definir uma adjacência 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 registos no arranque 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 contentor de fragmentos solicitou 2 fragmentos, os n.ºs 1 e 2. Tudo decorre, portanto, como se houvesse uma adjacência de 1. A adjacência de 0 foi, assim, ignorada.
1.9.3. Comunicação entre fragmentos
Na arquitetura anterior, temos uma atividade e n fragmentos. O utilizador interage com os diferentes fragmentos. Estas interações alteram o estado da aplicação. Denomina-se aqui «estado da aplicação» o conjunto de informações que esta armazena ao longo do seu ciclo de vida. Coloca-se então o seguinte problema:
- quando o utilizador interage com o fragmento i, a aplicação passa de um estado E1 para um estado E2;
- uma ação do utilizador no fragmento i faz com que o fragmento j seja apresentado;
- como atualizar o fragmento j com o estado atual E2 da aplicação;
Pelos exemplos anteriores, sabemos como atualizar o fragmento j. Mas onde encontrar o estado E2 da aplicação para o atualizar?
Existem várias soluções para este problema. Já vimos uma delas: o fragmento i pode transmitir o estado E2 da aplicação ao fragmento j através de argumentos. Encontrámos este 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 ao fragmento
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
Esta solução não é imediatamente aplicável aqui. Com efeito, quando o utilizador clica no separador j, o que faz com que o fragmento j seja exibido, o nosso código não é chamado. É apenas o código do sistema que é executado. Veremos num projeto futuro como interceptar o clique num separador, mas, por enquanto, vamos seguir outro caminho.
Já falámos sobre o estado da aplicação: o conjunto de dados geridos pela aplicação ao longo do tempo. Aqui, a aplicação é constituída por uma atividade e n fragmentos, todos instanciados uma única vez no arranque da aplicação e cuja duração corresponde à da própria aplicação. Assim, cada um destes elementos, ou vários em conjunto, podem ser candidatos a armazenar o estado da aplicação. Cada fragmento tem acesso, através 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 nessa atividade.
No entanto, o resultado do método [Fragment.getActivity()] depende do momento em que é chamado no ciclo de vida. Ilustramos este ponto adicionando alguns registos 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 realizar depende do número da visita
if (numVisit > 1) {
// registo
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 registos
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] apresenta parte do estado da aplicação;
Iniciamos a aplicação com uma adjacência de fragmentos igual a 2. Os registos no arranque 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: verifica-se que, nos métodos [setUserVisibleHint], ocorre a chamada de [getActivity()==null] se o fragmento ainda não estiver visível (isVisibleToUser==false);
- linha 19: verifica-se que, quando o fluxo de execução chega ao método [update] do fragmento 1, o método [getActivity] devolve corretamente a atividade;
Quando se define a adjacência dos fragmentos para 4 (adjacência total), os registos 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 -- accepting and dropping
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
Obtenem-se os mesmos resultados. Conclui-se que, assim que o fragmento fica visível, o método [getActivity] devolve a atividade do fragmento. Observa-se também que, quando a execução atinge o método [update] do fragmento que vai ser apresentado, o método [getActivity] devolve corretamente um valor.
Para ilustrar a comunicação entre fragmentos, vamos criar um novo projeto.
1.10. Exemplo-09: comunicação entre fragmentos, deslizar e deslocamento
1.10.1. Criação do projeto
Duplicamos o projeto [Exemple-07] em [Exemple-08]. Para tal, seguiremos o procedimento descrito para duplicar o [Exemple-02] em [Exemple-03] no parágrafo 1.4.
![]() | ![]() |
1.10.2. A sessão
Neste novo projeto, pretendemos que os fragmentos exibam o número total de fragmentos visualizados pelo utilizador. Para tal, é necessário manter um contador que seja acessível a todos os fragmentos. Chamaremos de «sessão» o objeto que encapsula os dados partilhados pelos fragmentos. Esta terminologia provém do desenvolvimento web, onde se colocam numa sessão os dados a partilhar entre diferentes vistas solicitadas pelo mesmo utilizador. O facto de encapsular as informações partilhadas pelos diferentes fragmentos num único objeto torna o código mais legível.
A classe [Session] terá a seguinte estrutura:
![]() |
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 irá encapsular o número de fragmentos visitados;
- linha 5: a anotação [EBean] é uma anotação AA. O atributo [scope] designa o âmbito (ou tempo de vida) da classe assim anotada. Aqui, o atributo [scope = EBean.Scope.Singleton] faz com que a classe [Session] seja um singleton: será instanciada uma única vez, no arranque da aplicação. A referência a uma classe anotada com [EBean] pode, posteriormente, ser injetada noutra classe. Trata-se do conceito de injeção de dependências;
1.10.3. A atividade [MainActivity]
A atividade [MainActivity] evolui da seguinte forma:
@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 através da anotação [@Bean]. O parâmetro da anotação é a classe do bean a injetar. O campo assim anotado não pode ter o âmbito [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 efetuadas. Assim, quando se entra no método [afterInject] da linha 16, a referência da linha 8 já foi inicializada;
- linha 20: o contador de visitas é colocado a zero;
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;
// registo
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 vai ser armazenado em cache
updateDone = false;
}
}
// atualização do fragmento
public void update() {
// registo
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, quando se chega ao método [update], o método [getActivity] devolve 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, vamos buscá-lo à sessão. Poderíamos ter colocado este código no método [setUserVisibleHint] a partir da linha 19, pois sabemos que, nessa altura, o método [getActivity] devolve a atividade. Decidimos aqui não atribuir qualquer papel específico a este método e transferir o código específico de um fragmento para o método [update], que foi concebido para esse efeito;
- linha 43: apresenta o número da visita;
Quando se executa esta aplicação com 5 fragmentos, com uma adjacência de 2 fragmentos, os primeiros registos 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: verifica-se que o método [afterInject] da atividade é executado antes do método [afterViews];
Convidamos o leitor a testar esta nova aplicação.
1.10.5. Desativar o Swipe ou Deslize
Na aplicação anterior, quando se desliza o rato para a esquerda ou para a direita no emulador Android, a vista atual dá lugar à vista da direita ou da esquerda, consoante o caso. Este comportamento por predefinição nem sempre é desejável. Vamos aprender a desativar o deslize entre vistas (swipe).
Voltemos à vista principal XML:
![]() |
No código XML da vista, encontramos o código do contentor 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 designa a classe que gere as páginas da atividade. Esta classe encontra-se na atividade [MainActivity]:
import android.support.v4.view.ViewPager;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// o gestor de fragmentos
private SectionsPagerAdapter mSectionsPagerAdapter;
// o contentor de fragmentos
@ViewById(R.id.container)
protected ViewPager mViewPager;
...
Na linha 12, o contentor de fragmentos é do tipo [android.support.v4.view.ViewPager] (linha 1). Para desativar a varredura, é necessário derivar esta classe da seguinte forma:
![]() |
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 redefinir para gerir 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 gestores de eventos das linhas 24 e 34 podem ser chamados. Ambos devolvem um valor booleano. Basta que devolvam o valor booleano [false] para inibir o deslizamento;
- linha 11: o valor booleano que serve para indicar se o gesto de deslizar com a mão é aceite ou não.
Feito isto, é necessário utilizar agora o nosso novo gestor de páginas. Isto é feito na vista 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 passa a ser o seguinte:
package exemples.android;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// o gestor de fragmentos
private SectionsPagerAdapter mSectionsPagerAdapter;
// o contentor de fragmentos
@ViewById(R.id.container)
protected MyPager mViewPager;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
// o contentor de fragmentos está associado ao gestor de fragmentos
// ou seja, o fragmento n.º i do contentor de fragmentos é o fragmento n.º i fornecido pelo gestor de fragmentos
mViewPager.setAdapter(mSectionsPagerAdapter);
// desativa-se o deslize entre fragmentos
mViewPager.setSwipeEnabled(false);
// a barra de separadores também está associada ao contentor de fragmentos
...
- linha 12: o gestor de páginas tem agora o tipo [MyPager];
- linha 23: permite ativar ou desativar o deslocamento com a mão.
Teste esta nova versão. Desative ou não a deslocação e observe a diferença de comportamento das vistas quando as arrasta para a direita ou para a esquerda com o rato. Em todas as aplicações futuras, a deslocação estará desativada. Não voltaremos a referir este assunto.
1.10.6. Desativar a rolagem entre fragmentos
Vamos continuar com uma melhoria no gestor de separadores. Quando se passa do separador 1 para o separador 4, vêem-se a deslocar-se os dois separadores intermédios, 2 e 3. Na gíria do Android, isto chama-se smoothScrolling. Este comportamento pode tornar-se incómodo se houver muitas abas. Pode ser desativado adicionando o seguinte código no gestor de fragmentos [MyPager]:
// controla o deslize
private boolean isSwipeEnabled;
// controla a rolagem
private boolean isScrollingEnabled;
...
// deslocamento
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// setter
...
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
Como o gestor de separadores foi associado ao gestor de fragmentos [MyPager], quando se clica no separador n.º i, o fragmento n.º i é apresentado pelo contentor de fragmentos através do método [setCurrentItem] acima referido (linha 9). [position] é o n.º do fragmento a apresentar;
- linha 10: é chamado o método [setCurrentItem] da classe pai. O segundo argumento de [false] solicita que haja uma transição imediata entre o fragmento antigo e o novo (sem deslocamento), enquanto o de [true] solicita que haja uma transição através de scrolling. Aqui, o segundo argumento é o valor do campo da linha 4, campo que o programador pode definir através do método das linhas 16-18;
Se se pretender desativar a rolagem, a classe [MainActivity] será a seguinte:
...
// deslocamento dos fragmentos
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// inibe o deslizar entre fragmentos
mViewPager.setSwipeEnabled(false);
// sem deslocamento
mViewPager.setScrollingEnabled(false);
...
Execute novamente o projeto e verifique se já não existe scrolling entre os separadores 1 e 4, por exemplo. Daqui em diante, iremos sempre desativar a rolagem. Não voltaremos a abordar este assunto.
1.10.7. Um novo fragmento
No nosso exemplo, todos os fragmentos são do mesmo tipo [PlaceHolderFragment]. Vamos agora aprender a criar um novo fragmento e a exibi-lo.
Primeiro, vamos copiar a vista [vue1.xml] do projeto [Exemple-04] para o projeto [Exemple-09] [1]:
![]() | ![]() |
- no [1], a vista [vue1.xml];
- em [3], a vista apresenta erros decorrentes de textos ausentes no ficheiro [res/values/strings.xml];
No ficheiro [2], adicionam-se os textos em falta, retirando-os do ficheiro [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>
<!-- 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>
</resources>
- acima, adicionámos as linhas 6 a 9;
Agora, criamos a classe [Vue1Fragment], que será o fragmento responsável por apresentar a vista [vue1.xml]:
![]() |
A classe [Vue1Fragment] terá o seguinte aspeto:
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;
// gestor de eventos
@Click(R.id.buttonValider)
protected void doValider() {
// exibe-se o nome introduzido
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 realidade, a classe [Vue1Fragment_]. É importante ter isto em conta. O fragmento está associado à vista [vue1.xml];
- linhas 14-15: o componente identificado por [R.id.editTextNom] é inserido no campo [editTextNom] da linha 15;
- linhas 18-20: o método [doValider] gere 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 se encontra. Trata-se de [MainActivity], uma vez que, nesta arquitetura, temos apenas uma atividade que apresenta diferentes vistas ou fragmentos;
Na classe [MainActivity], o gestor 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 ao 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: existe [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, em seguida, execute o projeto [Exemple-09]. O separador n.º 5 deve estar diferente:
![]() |
1.10.8. Derivar todos os fragmentos de uma mesma classe abstrata
O novo fragmento [Vue1Fragment] também precisa de ser atualizado quando é apresentado. Para tal, teremos de criar um código semelhante ao criado para o fragmento [PlaceholderFragment]. Para evitar repetições, vamos factorizar o que for possível numa classe abstrata da qual todos os fragmentos da aplicação irão herdar.
Para tal, 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. Gestão do modo de depuração
Adicionamos ao projeto a possibilidade de mostrar ou não os registos do modo de depuração. Para tal, 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();
// registo
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 à classe filha que se atualize
updateFragment();
}
protected abstract void updateFragment();
}
- linha 7: a classe [AbstractFragment] estende a classe Android [Fragment];
- qualquer fragmento deve poder atualizar-se. É por isso que a classe pai [AbstractFragment] impõe às suas classes filhas a presença de um método [updateFragment] (linha 68) que ela chama (linha 65);
- linha 19: a classe armazenará uma referência à atividade da aplicação;
- linha 22: a classe armazenará uma referência à sessão onde estão reunidos os dados partilhados 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: guarda-se o nome da classe instanciada, ou seja, o nome de uma classe filha;
- linhas 15-22: estes campos têm o atributo [protected] para que as classes filhas tenham acesso aos mesmos. Note-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 idênticos aos que existiam na classe [PlaceholderFragment] do projeto anterior:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// pai
super.setUserVisibleHint(isVisibleToUser);
// memória
this.isVisibleToUser = isVisibleToUser;
// registo
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
}
// caso em que o fragmento se torne 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;
// registo
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
}
}
@Override
public void onResume() {
// pai
super.onResume();
// registo
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() {
// recuperar 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 à classe filha que se atualize
updateFragment();
}
De acordo com o código acima, quando o método [update] de um fragmento é executado, este fica visível. Isto é importante porque significa que o método [Fragment.getActivity] devolve então uma referência à atividade da aplicação (ver parágrafo 1.10.8), o que permite, por sua vez, o acesso à sessão.
- linhas 4-10: inicializam-se a atividade e a sessão, caso ainda não o tenham feito;
- linha 12: chama-se o método [updateFragment] da classe filha. Quando este for executado, os campos [activity] e [session] aos quais 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 vista apresentada por um contentor 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();
// registo
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 esta arquitetura, a criação de um fragmento consiste em:
- escrever o método [@AfterViews], que serve para inicializar o fragmento durante o seu primeiro ciclo de vida ou para o reinicializar, caso tenha havido um [onDestroyView] anteriormente. A linha 39 é obrigatória para gerir corretamente o ciclo de vida do fragmento;
- escrever o método [updateFragment], que atualizará o fragmento imediatamente antes da sua exibição. Este método pode utilizar a sessão da sua classe pai;
- escrever os gestores de eventos do fragmento. É isso que iremos fazer 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;
// registo
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() {
// registo
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
// incrementar número de visitas
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// texto alterado
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
// informações locais para registos
protected String getLocalInfos() {
return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
numVisit, initDone, getActivity() == null);
}
- linhas 7 e 23: nos registos, 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;
// registo
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
}
}
// gestor de eventos
@Click(R.id.buttonValider)
protected void doValider() {
// exibe-se o nome introduzido
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// informações locais para registos
protected String getLocalInfos() {
return String.format("numVisit=%s", numVisit);
}
// atualização do fragmento
@Override
protected void updateFragment() {
// incremento do n.º 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, é necessário escrevê-lo para definir o valor booleano [afterViewsDone] como true, uma vez que esta informação é utilizada pela classe pai;
- linhas 42-49: o método [updateFragment] consiste em exibir uma mensagem curta que mostra o número da visita (linha 48) e em incrementar esse número na sessão (linhas 44-46);
Convidamos o leitor a testar este novo projeto.
Iremos retomar esta arquitetura em todos os projetos futuros:
- uma atividade e n fragmentos;
- todos os fragmentos estendem a classe [AbstractFragment];
- os dados a partilhar entre fragmentos e entre fragmentos e a atividade são colocados na classe [Session];
1.11.6. Associação entre separadores e fragmentos
Na classe [MainActivity], que gere os separadores, está escrito:
// a barra de separadores também está associada ao contentor de fragmentos
// ou seja, o separador n.º i apresenta o fragmento n.º i do contentor
tabLayout.setupWithViewPager(mViewPager);
A linha 3 associa o gestor de separadores ao contentor de fragmentos. Vimos uma consequência desta associação: quando o utilizador clica no separador n.º i, o contentor de fragmentos exibe o fragmento n.º i. Não vimos o inverso: quando se solicita ao contentor de fragmentos que exiba o fragmento n.º i, o separador n.º i é automaticamente selecionado.
Para ilustrar este comportamento, vamos adicionar as opções [Fragment 1, Fragment 2, ...] ao menu atual. Quando o utilizador clicar na opção [Fragment i], será solicitado ao contentor 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 da aplicação:
![]() | ![]() |
O conteúdo do ficheiro [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 ficheiro [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:
![]() |
A gestão do clique nestas opções de menu é feita na classe [MainActivity]:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// registo
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;
}
}
// artigo processado
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// alteração do fragmento apresentado
mViewPager.setCurrentItem(i);
}
}
- linha 2: o método [onOptionsItemSelected] é chamado quando ocorre um clique numa das opções do menu;
- linha 8: recupera-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] remete para o método [showFragment(i-1)] das linhas 41-45;
- linha 43: solicita-se ao contentor de fragmentos que apresente o fragmento solicitado;
- linha 42: verifica-se previamente se isso é possível (condição 1) e se é necessário (condição 2);
Convidamos o leitor a testar esta nova versão. Verifica-se que, quando se solicita a exibição do fragmento n.º i, este é efetivamente exibido e o separador n.º i é, por sua vez, selecionado.
Agora que vimos como funciona a associação entre separadores e fragmentos, vamos debruçar-nos sobre outro caso: aquele em que a gestão dos separadores está dissociada da gestão dos fragmentos. É o caso, por exemplo, quando há menos separadores do que fragmentos. Para ilustrar este novo caso de utilização, vamos criar um novo projeto.
1.12. Exemplo 11: separadores dissociados dos fragmentos
1.12.1. Criação do projeto
Duplicamos o projeto [Exemple-10] para [Exemple-11]:
![]() | ![]() |
1.12.2. Objetivos
A nova aplicação terá dois separadores:
- 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] selecionado pelo utilizador;
- 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 no segundo separador
private int numFragment;
// getters e setters
...
}
- linha 10: vamos gerir nós próprios o clique nos separadores. Quando se clica num separador, é necessário recuperar o fragmento que este exibia da última vez que foi selecionado. O campo [numFragment] irá memorizar o número do fragmento para o separador n.º 2, um número presente em [0, Fragments_COUNT-2]. Quando se clicar no separador n.º 2, iremos procurar na sessão o número do fragmento a apresentar;
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>
O separador n.º 2 exibirá um dos quatro fragmentos das linhas 9-24. O 5.º fragmento é o fragmento [Vue1Fragment], que será sempre exibido no separador n.º 1.
1.12.5. A classe [MainActivity]
A classe [MainActivity] deve agora gerir os separadores e a navegação entre eles, algo que até agora não fazia. O seu código evolui da seguinte forma:
// o gestor de separadores
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
...
@AfterViews
protected void afterViews() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
...
// sem deslocamento
mViewPager.setScrollingEnabled(false);
// exibição da Vista 1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
// inicialmente, existe apenas um separador
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// gestor de eventos
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// foi selecionada uma aba — altera-se o fragmento apresentado pelo contentor de fragmentos
...
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
...
}
- linha 17: o primeiro fragmento exibido pelo contentor de fragmentos será o fragmento [Vue1Fragment]. Por definição, este será o último fragmento do contentor;
- linhas 20-22: como não foi estabelecida qualquer associação entre os separadores e o contentor de fragmentos, temos de gerir os separadores nós próprios. Inicialmente, a barra de separadores [tabLayout] da linha 3 não tem nenhum separador;
- linha 20: criamos o primeiro separador;
- linha 21: atribuímos-lhe um título. Nos exemplos anteriores, o título dos separadores era o título dos fragmentos. Isso já acabou. Por isso, removemos o método [getPageTitle] do gestor de fragmentos. Já não precisamos dele:
// opcional — atribui um título aos fragmentos geridos
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
- linha 22: o separador criado é adicionado à barra de separadores. A nossa barra de separadores tem agora um separador. O que é que este separador apresenta? É importante compreender que os separadores e os fragmentos são dois conceitos independentes. O fragmento apresentado é sempre aquele escolhido pelo contentor de fragmentos. Se mudarmos de separador e não solicitarmos ao contentor que altere o fragmento apresentado, nada acontece: continua a ser apresentado o mesmo fragmento, mas o separador selecionado mudou. Portanto, neste caso, o fragmento apresentado é aquele escolhido na linha 17: o fragmento [Vue1Fragment];
- linhas 26-30: o método a escrever para gerir a mudança de separador pelo utilizador;
O método [onTabSelected] das linhas 26-30 é acionado assim que há uma mudança de separador (se o utilizador clicar num separador já selecionado, nada acontece). O seu código é o seguinte:
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (IS_DEBUG_ENABLED) {
Log.d("onglets", "onTabSelected");
}
// foi selecionada uma guia - altera-se o fragmento exibido pelo contentor de fragmentos
// posição da guia
int position = tab.getPosition();
// n.º do fragmento a exibir
int numFragment;
switch (position) {
case 0:
// n.º do fragmento [Vue1Fragment]
numFragment = FRAGMENTS_COUNT - 1;
break;
default:
// n.º do fragmento [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// visualização do fragmento
mViewPager.setCurrentItem(numFragment);
}
- linha 8: recupera-se a posição do separador em que se clicou. Aqui, será obtido um número 0 ou 1;
- linhas 12-15: se for a primeira separadora em que se clicou, prepara-se a exibição do fragmento [Vue1Fragment];
- linhas 16-18: nos outros casos (se foi clicada a aba n.º 2), preparamo-nos para voltar a apresentar o fragmento que estava a ser apresentado da última vez que a aba n.º 2 foi selecionada. O número desse fragmento tinha sido, nessa altura, guardado na sessão da aplicação;
- linha 21: solicita-se ao contentor de fragmentos que exiba o fragmento pretendido;
Vejamos agora a gestão das opções do menu (ainda em [MainActivity]):
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// registo
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: gestão das 4 opções do menu. Cada gestor chama o método [showFragment] com o n.º do fragmento a apresentar;
O método [showFragment] é o seguinte:
// o separador 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, cria-se
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 apresentado
mViewPager.setCurrentItem(i);
// o n.º do fragmento exibido é guardado na sessão
session.setNumFragment(i);
// seleciona-se o separador 2 — não faz nada se este já estiver selecionado
tab2.select();
}
}
- Recorde-se que, no início da aplicação, existe apenas um separador;
- linha 2: uma referência à guia n.º 2, null no início;
- linha 5: as condições de visualização não se alteraram em relação à versão anterior;
- linhas 7-10: se o separador n.º 2 ainda não existir, é criado (linha 8) e adicionado à barra de separadores (linha 9);
- linha 12: insere-se no título da segunda aba o n.º do fragmento que vai ser apresentado, com uma numeração que começa em 1;
- linha 14: o fragmento pretendido é apresentado;
- linha 16: o seu n.º é inserido na sessão;
- linha 18: a aba n.º 2 é selecionada. Se já estivesse selecionada, nada acontecerá: o método [onTabSelected] não será executado. Se ainda não estivesse selecionada, o método [onTabSelected] será acionado. Este método solicita então ao contentor de fragmentos que exiba o fragmento já exibido na linha 14. Um simples teste no método [onTabSelected] evita esta situação:
// exibição do fragmento apenas se for necessário
if (numFragment != mViewPager.getCurrentItem()) {
mViewPager.setCurrentItem(numFragment);
}
Convidamos o leitor a testar esta nova versão.
1.12.6. Melhorias
Temos agora uma boa compreensão dos fragmentos, do seu ciclo de vida, do conceito de adjacência entre fragmentos e da sua relação com a barra de separadores. Dispomos, além disso, de 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 partilhar entre fragmentos e entre fragmentos e a atividade são colocados na classe [Session];
Num novo projeto, iremos especificar as relações entre a atividade e os fragmentos através da adição de uma interface.
1.13. Exemplo 12: codificar as relações entre a atividade e os fragmentos
Neste exemplo, pretendemos definir as relações mínimas entre a atividade e os fragmentos. Para tal, 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 qualquer fragmento deverá possuir;
1.13.1. Criação do projeto
Duplicamos o projeto [Exemple-11] para [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, verifica-se que os fragmentos precisam de ter acesso à sessão instanciada pela atividade. Além disso, embora não seja visível nestes exemplos, é previsível que os gestores de eventos dos fragmentos terminem, por vezes, com uma mudança de vista. Solicitar-se-á à atividade que efetue essa mudança. A interface [IMainActivity] poderia, então, ser a seguinte:
![]() |
package exemples.android;
public interface IMainActivity {
// acesso à sessão
Session getSession();
// alteração da vista
void navigateToView(int position);
// modo de depuração
boolean IS_DEBUG_ENABLED = true;
}
Na linha 12, note-se a presença de uma constante que anteriormente se encontrava 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 ter um nome diferente de [MainActivity]. Uma vez que a constante [IS_DEBUG_ENABLED] é utilizada nos fragmentos, esta é transferida para a interface [IMainActivity].
1.13.3. A classe abstrata [AbstractFragment]
A classe abstrata [AbstractFragment] sofre poucas 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() {
// recuperam-se 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 à classe filha que se atualize
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]. É o caso de todas as atividades;
Estas duas referências apontam, naturalmente, para o mesmo objeto. No entanto, este é visto com dois tipos diferentes. Isto evitar-nos-á conversões de tipo durante a execução;
- linha 14: obtém-se uma referência à atividade através do método [getActivity];
- linha 15: se esta for diferente de nulo, então é possível aceder à sessão;
- linhas 16-17: guardamos a atividade como implementadora da interface [IMainActivity] e a sessão;
1.13.4. Alteração do gestor de fragmentos
O gestor de fragmentos [SectionsPagerAdapter] na classe [MainActivity] é alterado num único ponto: em vez de gerir fragmentos do tipo [Fragment], passa a gerir 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.º de posição do fragmento
@Override
public AbstractFragment getItem(int position) {
...
}
// retorna o número de fragmentos geridos
@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 da sessão
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// exibe a vista na 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] faz com que seja exibido o fragmento n.º [position];
- linha 17: verifica-se se há algo a fazer;
- linha 19: o fragmento n.º [position] é apresentado;
Nesta fase, execute a aplicação. Deve funcionar.
1.13.6. Alteração da exibição dos fragmentos em [MainActivity]
Atualmente, a classe [MainActivity] apresenta um fragmento através da instrução:
// exibição da Vista1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
Como o método [navigateToView] faz o mesmo, substitui-se este tipo de instrução em todos os locais (2 locais) por:
Em seguida, execute a aplicação. Deve continuar a funcionar.
1.13.7. Conclusão
A partir de agora, utilizaremos sempre 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]. Estes devem também possuir um método [@AfterViews], no qual definem o valor booleano [afterViewsDone] como true;
- uma sessão que encapsule os dados a partilhar entre fragmentos e a atividade;
1.14. Exemplo-13: Exemplo-05 com fragmentos
No projeto [Exemple-05], introduzimos a navegação entre vistas. Tratava-se, então, de uma navegação entre atividades: 1 vista = 1 atividade. Propomos aqui ter uma única atividade com várias vistas do tipo [AbstractFragment].
1.14.1. Criação do projeto
Duplicamos o projeto anterior [Exemple-12] para [Exemple-13], seguindo o procedimento descrito no parágrafo 1.4. Obtemos o seguinte resultado:
![]() | ![]() |
1.14.2. Estruturação do projeto
Vamos começar a utilizar pacotes para organizar o código. Por enquanto, podemos distinguir duas áreas distintas:
- a gestão 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 denominado [architecture], no qual colocaremos as entidades [IMainActivity, AbstractFragment, Session, MyPager], que constituem os elementos básicos da arquitetura da nossa aplicação. Isto serve para nos lembrar que fizemos uma escolha arquitetónica específica. Em seguida, mova os elementos existentes do projeto conforme indicado em [9]. Cada movimentação deve ser validada clicando no botão [Refactor].
Nesta fase, compile a aplicação. Temos os seguintes erros em [MainActivity:
![]() |
Ao mover as classes para os pacotes, o Android Studio efetuou as alterações necessárias no código da aplicação (linhas 18-21, por exemplo). As classes a que se referem as linhas 15 e 17 não foram movidas. São geradas pela biblioteca Android Annotations. Para estas classes, é necessário alterar manualmente os imports. Estas linhas passam, assim, a ser:
![]() |
Feito isto, já não há erros de compilação. Execute a aplicação. Surge então o seguinte erro:
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{exemples.android/exemples.android.MainActivity_}:
Este erro tem origem no manifesto da aplicação:
![]() |
<?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 passar a ser:
android:name=".activity.MainActivity_"
Tenha 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. A utilização da biblioteca AA acarreta, portanto, alguns inconvenientes.
1.14.3. Limpeza do projeto
No novo projeto:
- já não existem separadores, botões flutuantes nem menus;
- os fragmentos [PlaceholderFragment] desaparecem. A aplicação irá gerir dois fragmentos: o [Vue1Fragment], que já existe, e o [Vue2Fragment], que terá de ser criado;
- a sessão já não é a mesma;
1.14.3.1. Limpeza dos fragmentos
Elimine as classes [PlaceHolderFragment] e [1]:
![]() | ![]() |
Da mesma forma, elimine a vista [res / layout / fragment_main.xml] associada a este fragmento [2].
1.14.3.2. Limpeza da sessão
A sessão é atualmente a seguinte:
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] apresentado no segundo separador
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 desta sessão.
Compile o projeto. As linhas com erros são aquelas que utilizavam o conteúdo da sessão. Elimine-as. Na classe [Vue1Fragment], elimina-se também 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;
// registo
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gestor de eventos
@Click(R.id.buttonValider)
protected void doValider() {
// exibe-se o nome introduzido
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. Eliminação dos separadores, do botão flutuante e do menu
A remoção dos separadores e do botão flutuante é feita em dois locais:
- na vista [res / layout / activity-main.xml], que define estes elementos e a sua localização na vista;
- no código da atividade [MainActivity];
A remoção do menu também é efetuada 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 vista [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>
- eliminam-se as linhas [28-31, 41-47];
- elimina-se também a barra de ferramentas das linhas 18-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>
- vão-se eliminar as linhas 9 a 24. Deixa-se assim uma opção que não será utilizada. Apenas para ter um exemplo de declaração de uma opção de menu que se possa reproduzir através de copiar/colar;
Na classe [MainActivity], eliminamos tudo o que faz referência aos separadores, ao botão flutuante, à barra de ferramentas e ao menu. Para encontrar essas referências, o mais simples é eliminar a respetiva declaração:
// o gestor de separadores
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// o botão flutuante
@ViewById(R.id.fab)
protected FloatingActionButton fab;
e recompilar a aplicação. As linhas com erros são aquelas que fazem referência aos elementos que desapareceram. Elimine, então, todas essas linhas. Além disso, altere o gestor de fragmentos para que deixe de fazer referência ao fragmento [PlaceholderFragment] que eliminámos:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// os fragmentos
private AbstractFragment[] fragments;
// construtor
public SectionsPagerAdapter(FragmentManager fm) {
// pai
super(fm);
}
// posição do fragmento n.º
@Override
public AbstractFragment getItem(int position) {
// registo
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// retorna o número de fragmentos geridos
@Override
public int getCount() {
return fragments.length;
}
}
- linhas 7-10: eliminámos toda a geração de fragmentos;
Nesta fase, já não deve haver erros de compilação. Na classe [MainActivity], chegámos ao seguinte código intermédio:
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 contentor 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 gestor de fragmentos
private SectionsPagerAdapter mSectionsPagerAdapter;
// construtor
public MainActivity() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "constructor");
}
}
@AfterViews
protected void afterViews() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
// barra de ferramentas — é aqui que é exibido o nome da aplicação
setSupportActionBar(toolbar);
// o gestor de fragmentos
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// o contentor de fragmentos está associado ao gestor de fragmentos
// ou seja, o fragmento n.º i do contentor de fragmentos é o fragmento n.º i fornecido pelo gestor de fragmentos
mViewPager.setAdapter(mSectionsPagerAdapter);
// deslocamento dos fragmentos
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// desativa-se o deslize entre fragmentos
mViewPager.setSwipeEnabled(false);
// sem deslocamento
mViewPager.setScrollingEnabled(false);
// visualização Vista1
navigateToView(FRAGMENTS_COUNT - 1);
}
@AfterInject
protected void afterInject() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
}
// getter da sessão
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// exibe a vista «position»
if (mViewPager.getCurrentItem() != position) {
// exibição do fragmento
mViewPager.setCurrentItem(position);
}
}
// o gestor de fragmentos
// é a ele que se solicitam os fragmentos a exibir na vista principal
// deve definir os métodos [getItem] e [getCount] — os restantes 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) {
// registo
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// retorna o número de fragmentos geridos
@Override
public int getCount() {
return fragments.length;
}
}
}
Ainda há algumas alterações a fazer:
- elimine a linha 31, que já não tem razão de ser;
- linha 33: defina 1 como adjacência de fragmentos;
- linha 76: navegue para a vista 0. Será esta que será apresentada em primeiro lugar;
- linha 108: inicialize a matriz com o fragmento [Vue1Fragment_]:
// os fragmentos
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
Portanto, temos apenas um fragmento. Execute a aplicação. Deve obter o seguinte resultado:

O botão [Valider] deve funcionar.
1.14.4. Criação dos fragmentos e das vistas associadas
A aplicação terá duas vistas, as do projeto [Exemple-05]. Já temos a vista [vue1.xml] no projeto atual. Vamos agora duplicar a vista [vue2.xml], copiando-a de [Exemple-05] para [Exemple-12] (abra os dois projetos e faça copiar/colar) entre eles.
![]() | ![]() |
- em [1], a nova vista. Quando se tenta editá-la, surgem erros no ficheiro [2]. É necessário modificar o ficheiro [strings.xml] e [3] para adicionar as cadeias de caracteres referenciadas por esta nova vista:
<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>
<!-- 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>
<!-- vista 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 alteramos o código copiado da seguinte forma:
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;
// registo
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// atualização do fragmento
@Override
protected void updateFragment() {
}
}
- linha 9: o fragmento está associado à vista [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 irá agora gerir dois fragmentos. A 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] assegura a navegação entre vistas com o seu método [navigateToView]. Vamos gerir 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;
// registo
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gestores de eventos ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// é exibido o nome introduzido
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] gere o evento «clique» no botão [Vue n° 2];
- linha 39: a navegação é efetuada com o método [navigateToView] da atividade. Recorde-se aqui que a atividade foi memorizada na classe pai da seguinte forma:
// atividade
protected IMainActivity mainActivity;
e que esta atividade já foi inicializada ao entrar em qualquer gestor de eventos.
- linha 34: a instrução utiliza a variável [activity] da classe pai, que é uma referência à atividade enquanto 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;
// registo
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gestores 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] gere o evento «clique» no botão [Vue n° 1];
Execute o projeto e verifique se a navegação entre as vistas funciona.
1.14.6. Definição da sessão
O funcionamento da aplicação é o seguinte:
- introdução de um nome na vista n.º 1;
- exibição desse nome na vista n.º 2;
Para que a vista n.º 1 possa transmitir o nome introduzido à vista 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 introduzido;
A classe [MainActivity] irá inicializar a sessão da seguinte forma:
// injeção de sessão
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// inicialização da sessão
session.setNom("");
}
1.14.7. Escrita final dos fragmentos
No fragmento [Vue1Fragment], alteramos o código do gestor do 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;
...
// gestores de eventos ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// memoriza-se o nome introduzido
String nom = editTextNom.getText().toString();
// exibe-se
Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
// o nome introduzido é guardado na sessão
session.setNom(editTextNom.getText().toString());
// navega-se para a vista n.º 2
mainActivity.navigateToView(1);
}
// atualização do fragmento
@Override
protected void updateFragment() {
}
}
- linhas: 31-37: gerimos o clique no botão [Vue n° 2];
- linha 34: antes de navegar para a vista n.º 2, colocamos o nome introduzido na sessão para que a nova vista tenha acesso ao mesmo;
A vista [Vue2Fragment] evolui 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;
// registo
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gestores de eventos ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// atualização de fragmento
@Override
protected void updateFragment() {
// recupera-se o nome introduzido na sessão
String nom = session.getNom();
// exibe-se
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
Quando a vista n.º 2 for apresentada, é necessário apresentar o nome introduzido na vista n.º 1. Sabe-se que, imediatamente após a sua apresentação, o seu método [updateFragment] será executado. É, portanto, neste método (linhas 36-42) que se pode inserir o código para apresentar o nome.
- linhas 16-17: declaração do único componente visual da vista;
- linha 39: o nome introduzido na vista n.º 1 é recuperado da sessão;
- linha 41: o rótulo [textViewBonjour] é alterado;
Execute o projeto e verifique se funciona.
1.14.8. Gestão do ciclo de vida dos fragmentos
No fragmento [Vue1Fragment], o método [@AfterViews] é o seguinte:
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// registo
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
Este método está incompleto. Com efeito, é sempre necessário prever o caso em que o fragmento é reciclado após uma operação [onDestroyView]. Nesse caso, a vista do fragmento 1 é regenerada e o nome que possa ter sido introduzido anteriormente desaparecerá da vista. Não queremos que isso aconteça. Atualmente, o nome introduzido permanece visível 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 reciclagem do fragmento.
Existem várias formas de resolver este problema:
- pode-se aproveitar o facto de o método [update] ser executado sistematicamente sempre que o fragmento é apresentado para atualizar o nome introduzido;
- pode-se efetuar essa atualização apenas quando o método [@AfterViews] for reexecutado. É esta última opção que adotamos;
Alteramos o código de [Vue1Fragment] da seguinte forma:
// elementos da interface visual
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// dados
private String nom;
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// registo
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// (re)inicializa o texto apresentado
editTextNom.setText(nom);
}
// gestores de eventos ----------------------------------
...
@Click(R.id.buttonVue2)
protected void showVue2() {
// regista-se o nome introduzido para poder recuperá-lo caso o fragmento seja reciclado
nom = editTextNom.getText().toString();
// coloca-se o nome introduzido na sessão
session.setNom(nom);
// navega-se para a vista n.º 2
activity.navigateToView(1);
}
- linha 27: quando nos preparamos para sair da vista 1 para a vista 2, guardamos o nome introduzido;
- linha 17: a cada nova execução do ciclo de vida do fragmento, o último nome introduzido é novamente apresentado;
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;
// registo
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// atualização do fragmento
@Override
protected void updateFragment() {
// recupera-se o nome introduzido na sessão
String nom = session.getNom();
// é exibido
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
- o único componente visual da vista (linha 3) é atualizado sempre que a vista é apresentada (linha 21). O método [@AfterViews] não tem, portanto, nada a acrescentar;
1.14.9. Conclusão
Chegados a este ponto, demonstrámos mais uma vez a pertinência da 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]. Estes devem também possuir um método [@AfterViews], no qual definem o valor booleano [afterViewsDone] como true;
- uma sessão que encapsule os dados a partilhar entre fragmentos e a atividade;
1.15. Exemplo 14: uma arquitetura de duas camadas
Vamos construir uma aplicação de uma única vista com a seguinte arquitetura:
![]() |
1.15.1. Criação do projeto
Duplicamos o projeto anterior [Exemple-12] para [Exemple-13], seguindo o procedimento descrito no parágrafo 1.4. Obtemos o seguinte resultado:
![]() | ![]() |
1.15.2. A vista [vue1]
A aplicação terá apenas uma vista [vue1.xml]. Por isso, eliminamos a outra vista [vue2.xml], bem como o seu fragmento associado:
![]() | ![]() |
Compile a aplicação. Surgem erros na vista [MainActivity]:
![]() |
Corrija a linha 4 abaixo no gestor 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_()};
Elimine as importações que se tornaram desnecessárias: [Ctrl-Shift-O]. Já não deve haver erros de compilação. Execute o projeto: a vista n.º 1 deve aparecer. Vamos agora modificá-la.
Vamos criar a vista [vue1.xml], que permitirá gerar números aleatórios:
![]() |
Os seus componentes são os seguintes:
O 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 vista anterior utiliza rótulos definidos no ficheiro [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>
<!-- vista 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 ficheiro [vue1.xml] estão definidas no ficheiro [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 da aplicação -->
<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, existe apenas um fragmento, não é necessário prever comunicação entre fragmentos. A sessão ficará, portanto, vazia:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
}
Nesta fase, compile a aplicação. Surgirão erros nas linhas que utilizavam elementos da sessão, agora vazia. Elimine essas linhas e verifique se a compilação já não produz erros.
1.15.4. O fragmento [Vue1Fragment]
![]() |
Alteramos o fragmento [Vue1Fragment] existente da seguinte forma:
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;
// os dados introduzidos
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
// memória
afterViewsDone = true;
// registo
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// ocultar as mensagens de erro
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
}
@Click(R.id.btn_Executer)
void doExecuter() {
// ocultam-se eventuais mensagens de erro anteriores
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// verifica-se a validade dos dados introduzidos
if (!isPageValid()) {
return;
}
}
// verifica-se a validade dos dados introduzidos
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// registo
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
}
}
- Aqui existe apenas um fragmento cujo ciclo de vida será executado uma única vez, no arranque da aplicação. Por este motivo, os métodos [@AfterViews] (linhas 46-57) e [udateFragment] (linhas 75-81) serão executados apenas uma vez no arranque da aplicação;
- linhas 55-56: ocultam-se as duas mensagens de erro da vista (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 introduzidos;
O método [isPageValid] é o seguinte:
// os dados introduzidos
private int nbAleas;
private int a;
private int b;
...
// verifica-se a validade dos dados introduzidos
private boolean isPageValid() {
// introduçã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);
}
// introduçã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);
}
// introduçã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: estes três campos são inicializados pelo método [isPageValid]. Além disso, este método retorna true se todas as entradas forem válidas e false caso contrário. Se houver entradas inválidas, são exibidas as mensagens de erro associadas;
Nesta fase, a aplicação está operacional. Verifique o funcionamento do método [isPageValid] introduzindo 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)] devolve normalmente n números inteiros aleatórios no intervalo [a,b]. Prevê-se também que, uma em cada três vezes, o método devolva uma exceção, a qual é igualmente incluída nas respostas fornecidas pelo método. Por fim, este devolve uma lista de objetos do tipo [Exception] ou [Integer].
A implementação [Metier] desta 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;
}
// Geram-se números aleatórios
Random random = new Random();
for (int i = 0; i < n; i++) {
// gera-se uma exceção aleatória 1 vez em cada 3
int nombre = random.nextInt(3);
if (nombre == 0) {
réponses.add(new AleaException("Exception aléatoire"));
} else {
// caso contrário, devolve-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. Assim, é sempre a mesma referência que é injetada, mesmo que seja injetada várias vezes na camada [Présentation];
- o resto do código é convencional;
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 de sistema [RuntimeException], o que a torna uma exceção não controlada: não é necessário tratá-la num try/catch, nem incluí-la na assinatura dos métodos;
1.15.6. A atividade [MainActivity] revista
![]() |
Camada
[metier]
Atividade
Vista
Utilizador
A atividade irá implementar a interface [IMetier] da camada [métier]. Assim, o fragmento/vista terá apenas a atividade como interlocutor.
A atividade [MainActivity] já implementa a interface [IMainActivity]. Para que ela implemente também a interface [IMetier], pode-se:
- adicionar a interface [IMetier] às interfaces implementadas pela atividade;
- fazer com que a interface [IMainActivity], por sua vez, estenda a interface [IMetier]. É esta a abordagem que adotamos;
A interface [IMainActivity] passa a ter a seguinte forma:
![]() |
package exemples.android.architecture;
import exemples.android.metier.IMetier;
public interface IMainActivity extends IMetier {
// acesso à sessão
Session getSession();
// mudança de vista
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 tal, utiliza-se a anotação AA [@Bean], cujo parâmetro é a classe que contém a anotação AA [@EBean];
- linha 2: a atividade implementa a interface [IMainActivity] e, por conseguinte, a interface [IMetier] da camada [métier];
- linhas 16-19: implementação do único método da interface [IMetier]. Limita-se a delegar a chamada à camada [métier];
1.15.7. O fragmento [Vue1Fragment] revisto
![]() |
O código da classe [Vue1Fragment] evolui da seguinte forma:
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;
// os dados introduzidos
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 introduzidos
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// registo
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
// só será executado uma vez, no arranque da aplicação
// cria-se o adaptador do ListView — para tal, é 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 apresentar uma lista de elementos. Faz-o através de um adaptador do tipo [ListAdapter], que, por sua vez, está ligado à 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)
O [ListAdapter] é uma interface. A classe [ArrayAdapter] é uma classe que implementa esta interface. O construtor utilizado na linha 69 acima é o seguinte:
- [context] é a atividade que apresenta o [ListView];
- [resource] é o número inteiro que identifica a vista utilizada para apresentar um elemento do [ListView]. Esta vista pode ter qualquer grau de complexidade. É o programador que a constrói de acordo com as suas necessidades;
- [textViewResourceId] é o número inteiro que identifica um componente [TextView] na vista [resource]. A cadeia de caracteres será apresentada por este componente;
- [objects]: a lista de objetos exibidos pelo [ListView]. O método [toString] dos objetos é utilizado para apresentar o objeto na vista [TextView], identificada por [textViewResourceId], na vista identificada por [resource].
A tarefa do programador consiste em criar a vista [resource] que irá apresentar cada elemento do [ListView]. Para o caso simples em que se pretende exibir apenas uma simples cadeia de caracteres, como aqui, o Android fornece a vista identificada por [android.R.layout.simple_list_item_1]. Esta contém um componente [TextView] identificado por [android.R.id.text1]. Este é o método utilizado na linha 69 para criar o adaptador do [ListView]. Este adaptador só precisa de ser definido uma vez. Para permitir a sua reutilização, foi definido como variável de instância da classe (linha 39). Vejamos novamente a 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 num fragmento pelo [getActivity] e que foi aqui armazenada na variável [activity] da classe pai. Este campo nem sempre tem um valor. Assim, os registos mostram que, quando se chega ao método [@AfterViews], este ainda não foi inicializado e, por isso, não é possível inserir as linhas 69-70 neste método. No método [updateFragment], isso é possível porque sabemos que, quando este método é executado, temos necessariamente 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]. O 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);
// apagam-se as respostas anteriores
reponses.clear();
adapterReponses.notifyDataSetChanged();
// verifica-se a validade dos dados introduzidos
if (!isPageValid()) {
return;
}
// solicitam-se os números aleatórios à atividade
List<Object> data = mainActivity.getAleas(a, b, nbAleas);
// cria-se uma lista de strings a partir destes dados
for (Object o : data) {
if (o instanceof Exception) {
reponses.add(((Exception) o).getMessage());
} else {
reponses.add(o.toString());
}
}
// atualizar a lista
adapterReponses.notifyDataSetChanged();
}
- linhas 7-8: pretende-se esvaziar o ListView. Para tal, esvazia-se a fonte de dados [reponses] e solicita-se ao adaptador associado ao ListView que se atualize;
- linhas 10-12: antes de executar a ação solicitada, verifica-se se os valores introduzidos 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 apresenta;
- linha 24: solicita-se ao adaptador do ListView que se atualize;
1.15.8. Execução
Execute o projeto e verifique se está a funcionar corretamente.
1.16. Exemplo 15: arquitetura cliente/servidor
Abordamos uma arquitetura comum para uma aplicação Android, aquela em que a aplicação Android comunica com serviços web remotos. Teremos agora a seguinte arquitetura:
![]() |
Adicionámos à aplicação Android uma camada [DAO] para comunicar com o servidor remoto. Esta camada irá comunicar com o servidor que gera os números aleatórios apresentados pelo tablet Android. Este servidor terá a seguinte arquitetura de duas camadas:
![]() |
Os clientes consultam determinados URL da camada [web / jSON] e recebem uma resposta de texto no formato jSON (JavaScript Object Notation). Aqui, o nosso serviço web irá processar uma única URL do tipo [/a/b], que devolverá um número aleatório no intervalo [a,b]. Vamos descrever a aplicação pela seguinte ordem:
O servidor
- a sua camada [métier];
- o seu serviço [web / jSON] implementado com o Spring MVC;
O cliente
- a sua camada [DAO]. Não haverá camada [métier];
1.16.1. O servidor [web / jSON]
Pretendemos 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/]. Acedemos ao 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, por enquanto, qualquer ajuda:
![]() |
- em [1]: escolha um projeto Gradle;
- em [2-3]: as características da dependência jar gerada pelo projeto (ver 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. É então gerado um ficheiro zip com um esqueleto de projeto Gradle, que fica disponível para download;
O que colocar em [2-3]? Já utilizámos dependências Gradle. A do projeto anterior era, por exemplo, a seguinte:
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Desde a versão 0.11 do plugin Gradle para Android, é necessário utilizar o android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
...
}
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 apresenta-se na forma [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 ficheiro zip acima referido:
![]() | ![]() ![]() | ![]() |
No Android Studio, abra o projeto Gradle [server-01] [1-2]. O projeto aberto é o [3] (perspetiva «Project»).
1.16.1.2. Configuração do Gradle
![]() |
O ficheiro 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 destinam-se ao Eclipse IDE. Eliminamo-las;
- 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/]. Este plugin define as versões das dependências mais frequentemente utilizadas com o Spring. Isto permite não especificar as respetivas 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 utilizar, neste caso a versão 1.8;
- linhas 25-27: os repositórios de binários a utilizar para descarregar 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: esta dependência inclui todos os binários necessários para construir um serviço web Spring;
- linha 31: esta dependência inclui todos os binários necessários para os testes, nomeadamente para os testes JUnit;
- uma dependência [compile] indica que precisamos dessa dependência para compilar o projeto. Uma dependência [testCompile] indica que precisamos dessa dependência apenas para a execução dos testes. Nesse caso, não é incluída no binário do projeto;
Fazemos uma primeira limpeza do ficheiro Gradle:
// Spring Boot
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
// plugins
apply plugin: 'java'
apply plugin: 'spring-boot'
// binário do projeto
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
// versões 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: adicionámos o repositório Maven local da estação de desenvolvimento. Este é criado quando se instala o Maven (ver parágrafo 6.10). Se a dependência solicitada já se encontrar no repositório Maven local, não será solicitada ao repositório Maven central;
- linhas 19-22: uma tarefa 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 ficheiro [build.gradle] (o [1] encontra-se no canto superior direito e ao lado do IDE);
A operação anterior cria o arquivo jar do projeto e coloca-o na pasta [build / libs] [5]:
![]() |
O nome do arquivo provém diretamente das informações fornecidas à tarefa [jar] no ficheiro [build.gradle] (linhas 19-22).
O conjunto de dependências do projeto pode ser visualizado da seguinte forma:
![]() |
Pode-se verificar no [1] que a única dependência do projeto [compile('org.springframework.boot:spring-boot-starter-web')] trouxe consigo dezenas de ficheiros binários. O Spring Boot para a Web incluiu as dependências de que uma aplicação Web Spring MVC provavelmente irá necessitar. Isto significa que algumas podem ser desnecessárias. O Spring Boot é ideal para um tutorial:
- traz as dependências de que provavelmente vamos precisar;
- inclui um servidor Tomcat incorporado [1], o que nos poupa de ter de implementar a aplicação num servidor web externo;
É possível encontrar muitos exemplos que utilizam o Spring Boot no site do ecossistema Spring [http://spring.io/guides].
Vamos agora preencher o ficheiro [build.gradle] da seguinte forma:
// 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 conforme as normas do Maven no repositório Maven local
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 Gradle chamado [maven-publish], que permite publicar o binário do projeto num repositório Maven, respeitando as normas do Maven;
- linha 11: uma tarefa Gradle chamada [publishing];
- linhas 14-15: as características do binário Maven que vai ser criado;
- linha 23: o repositório Maven no qual 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 é criado e instalado na pasta indicada na linha 23 do ficheiro [build.gradle]:
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |
![]() |
A tarefa [jar] permite gerar o ficheiro binário do projeto. Este ficheiro binário não inclui as suas dependências, pelo que não é executável. É possível gerar um ficheiro binário com todas as suas dependências e que seja executável. Para tal, adicionamos ao ficheiro [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 indicar o nome completo da classe executável do projeto:
![]() |
O código desta 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 ficheiro binário é gerado na pasta [build / libs] e pode ser executado [1-7]:
![]() | ![]() |
1.16.1.3. Configuração do projeto
A configuração do Gradle não é suficiente. Também é necessário configurar o projeto. Como não se trata de um projeto Android gerado pelo IDE, esta configuração, que até agora não tínhamos feito, deve agora ser realizada.
![]() | ![]() |
- no [3-4]: utilize um JDK 1.8;
Para compilar o projeto, o botão disponível para projetos Android já não está presente. Utilizaremos uma opção do menu [1-2]:
![]() | ![]() |
A seguir, o leitor é convidado a criar o projeto que se segue. 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. 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 esta 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 comentamos a classe: é análoga à encontrada no exemplo anterior, com a diferença de que não lança exceções aleatoriamente. Salienta-se apenas, na linha 8, a anotação Spring [@Service], que fará com que o Spring instancie a classe numa única instância (singleton) e torne a sua referência disponível para outros componentes Spring. Outras anotações Spring poderiam ter sido utilizadas aqui para o mesmo efeito. Os componentes Spring têm nomes por predefinição que podem ser especificados como atributo da anotação utilizada. Sem esse atributo, como neste caso, o componente Spring recebe o nome da classe com o primeiro caractere em minúscula. Assim, neste caso, o componente Spring recebe, por predefiniçã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 – Vista – Controlador) da seguinte forma:
![]() |
O processamento de um pedido de um cliente decorre da seguinte forma:
- pedido — os URL solicitados têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... A [Dispatcher Servlet] é a classe do Spring que processa os URL recebidos. Esta «encaminha» o URL para a ação que deve processá-lo. Estas ações são métodos de classes específicas denominadas [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 a servlet [Dispatcher Servlet] lhe transmitiu. Estes 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 juntamente com o seu pedido;
- No processamento do pedido do utilizador, a ação pode necessitar da camada [metier] [2b]. Uma vez processado o pedido do cliente, este pode gerar várias respostas. Um exemplo clássico é:
- uma página de erro, caso a solicitação não tenha podido ser processada corretamente
- uma página de confirmação, caso contrário
- a ação solicita que uma determinada vista seja apresentada: [3]. Esta vista irá apresentar dados a que se chama o modelo da vista. É o M de MVC. A ação irá criar este modelo M [2c] e solicitar que uma vista V seja apresentada [3];
- resposta — a vista V selecionada utiliza o modelo M criado pela ação para inicializar as partes dinâmicas da resposta HTML que deve enviar ao cliente e, em seguida, envia essa resposta.
Para um serviço web / jSON, a arquitetura anterior é ligeiramente alterada:
![]() |
- em [4a], o modelo, que é uma classe Java, é transformado numa cadeia jSON por uma biblioteca jSON;
- em [4b], esta cadeia jSON é enviada para o navegador;
Um exemplo de serialização de um objeto Java para a cadeia de caracteres jSON e de deserialização de uma cadeia de caracteres jSON para um objeto Java é apresentado nos anexos do parágrafo 6.14.
Voltemos à camada [web] da nossa aplicação:
![]() |
Na nossa aplicação, existe apenas um controlador:
![]() |
O serviço web / jSON enviará aos seus clientes uma resposta do tipo [Response] com o seguinte conteúdo:
package exemples.android.server.web;
import java.util.List;
public class Response<T> {
// ----------------- propriedades
// estado da operação
private int status;
// eventuais mensagens de erro
private List<String> messages;
// o corpo da resposta
private T body;
// construtores
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters e setters
...
}
- linha 13: o campo [T body] é a resposta esperada pelo cliente. Decidimos utilizar aqui uma resposta genérica do tipo T, em vez do tipo Integer do número aleatório esperado. Queremos poder reutilizar esta classe noutras situações. Durante o processamento do pedido do cliente, o servidor pode deparar-se com um problema que é então resumido nos outros dois campos;
- linha 8: um código de estado (0 se não houver erro);
- linha 9: se o status for !=0, uma lista de mensagens de erro, normalmente 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ócio
@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ócio
try {
response.setBody(metier.getAlea(a, b));
response.setStatus(0);
} catch (AleaException e) {
response.setStatus(e.getCode());
response.setMessages(getMessagesFromException(e));
}
// enviamos 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 tratam de pedidos para determinadas URL da aplicação web;
- linhas 21-22: a anotação [@Autowired] solicita ao Spring que injete no campo um componente do tipo [IMetier]. Será a classe [Metier] anterior. É porque colocámos nesta a anotação [@Service] que ela é tratada como um componente Spring;
- linhas 24-25: fazemos o mesmo com um mapeador jSON que definiremos posteriormente. O nosso serviço web irá enviar a sua resposta sob a forma de uma cadeia jSON. É este mapeador que irá realizar a serialização da resposta em jSON;
- linha 30: o método que gera o número aleatório. O seu nome não tem importância. Quando é executado, os seus parâmetros foram inicializados pelo Spring MVC. Veremos como. Além disso, se for executado, é porque o servidor web recebeu um pedido HTTP GET para o URL da linha 28;
- linha 28: a anotação [@RequestMapping] define certas propriedades do método anotado:
- [value]: o URL aceite pelo método;
- [method]: o método HTTP aceite pelo método. Existem principalmente duas, GET e POST. O método [POST] é utilizado quando o cliente pretende anexar um documento ao seu pedido HTTP;
- [produces]: define um dos cabeçalhos da resposta HTTP que será enviada ao cliente. Neste caso, nos cabeçalhos HTTP enviados com a resposta ao cliente, haverá um que lhe indicará que a resposta lhe é enviada sob a forma de uma cadeia jSON. Este cabeçalho não é obrigatório. É fornecido a título informativo ao cliente, caso este esteja à espera de respostas que possam assumir diversas formas;
- [consumes]: não está presente aqui. Permite indicar os cabeçalhos HTTP que devem acompanhar a solicitação HTTP do cliente para que esta seja aceite;
- linha 29: a anotação [@ResponseBody] indica que o resultado produzido pelo método deve ser enviado ao cliente. Sem esta anotação, a resposta do método é considerada como uma chave que permite selecionar a página HTML a enviar ao cliente. Num serviço web / jSON, não existem 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. Isto é feito através da anotação @PathVariable("x"). Note-se que {a} e {b} são componentes de um URL e são, portanto, do tipo String. A conversão de String para o tipo dos parâmetros pode falhar. O Spring MVC lança então uma exceção. Resumindo: se, num 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]. Recorde-se 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 corresponde à 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>] é devolvida sob a forma de uma cadeia jSON;
1.16.1.6. Configuração do projeto Spring
![]() |
Existem várias formas de configurar o Spring:
- com ficheiros XML;
- com código Java;
- com uma combinação dos dois;
Optamos por configurar a nossa aplicação web com código Java. É a seguinte classe [Config] que assegura esta 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 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: indicamos ao Spring em que pacotes irá encontrar os dois componentes que deve gerir:
- 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 ele próprio uma série de configurações padrão para uma aplicação Spring MVC. Isto alivia, por sua vez, o trabalho do programador;
- linhas 16, 22, 27 e 33: a anotação [@Bean] também define componentes (beans) Spring, tal como as duas anotações mencionadas anteriormente (@Service, @Controller). Aqui, a anotação [@Bean] anota um método e não uma classe, sendo que o resultado do método 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 todos os pedidos dos clientes e que os distribui (daí o seu nome) pelos 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 aceites 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 este funcionará na porta 8080. Por predefinição, os ficheiros binários deste servidor web são fornecidos pela dependência [org.springframework.boot:spring-boot-starter-web] do ficheiro 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 da aplicação
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 irá iniciar a aplicação. O seu primeiro parâmetro é a classe Java que configura o projeto. Neste caso, a classe [Config] que acabámos de descrever. O segundo parâmetro é o tabuleiro de argumentos passado ao método [main] (linha 7);
É possível iniciar a aplicação web de várias formas, incluindo a seguinte:
![]() |
Na consola, aparecem então vários registos:
- linhas 12-14: o servidor Tomcat incorporado é iniciado;
- linhas 15-19: o servlet [DispatcherServlet] do Spring MVC é carregado e configurado;
- linha 20: a URL [/{a}/{b}] do servidor web é detetada;
Agora, vamos abrir um navegador e testar o URL do serviço web / jSON:
![]() |
![]() |
![]() |
![]() |
Obtenemos, em cada caso, a representação jSON de um objeto do tipo [Response<Integer>].
Em vez de utilizar 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], envia-se o pedido;

- em [4], os cabeçalhos HTTP da resposta do servidor. Note-se que esta indica que o documento enviado é uma cadeia jSON;
- em [5], a cadeia jSON recebida;
1.16.1.8. Geração do ficheiro jar executável do projeto
No parágrafo 1.16.1.2, mostrámos como configurar o ficheiro Gradle para gerar um executável da aplicação com todas as suas dependências. Adaptada à presente aplicação, esta configuração passa a ser a seguinte:
// 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 este executável, pode-se proceder da seguinte forma [1-5]:
![]() | ![]() |
Para o executar, deve-se parar o serviço web, caso esteja em execução ([1]), e, em seguida, executar o arquivo ([2-4]):
![]() | ![]() |
Abra um navegador e aceda a URL e [localhost:8080/100/200]. Deve obter os mesmos resultados que anteriormente.
1.16.1.9. Gestão de registos
Ao executar o ficheiro executável, verifica-se que não se obtêm os mesmos registos que ao executar o projeto a partir do IDE. Obtêm-se registos 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 gerir o nível dos registos adicionando um ficheiro [logback.xml] na pasta [resources] do projeto:
![]() |
Este ficheiro poderá ter o seguinte conteúdo:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- aos codificadores é atribuído, por predefiniçã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>
<!-- controlo do nível dos registos -->
<root level="info"> <!-- informação, depuração, aviso -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
O nível dos registos é controlado na linha 12. Se, agora, regenerarmos o arquivo executável e o executarmos, teremos apenas registos 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] (vista + atividade) semelhante à que analisámos no exemplo [Exemple-14];
- a camada [DAO], que se liga ao serviço [web / jSON] que analisámos anteriormente.
1.16.2.1. Criação do projeto
Duplicamos o projeto anterior [Exemple-14] para [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 ficheiro [build.gradle] é o seguinte:
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Desde a versão 0.11 do plugin Gradle do Android, é necessário utilizar o android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
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 produzir o APK
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
apt "org.androidannotations:rest-spring:$AAVersion"
compile "org.androidannotations:rest-spring-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
Apenas comentamos 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 assegurar as comunicações cliente/servidor. A versão [2.0.0.M3] é uma versão denominada «milestone», que não se encontra nos repositórios Maven habituais. Por isso, é necessário especificar, nas linhas 56-59, o repositório a utilizar (linha 58) para localizar a biblioteca;
- linha 51: uma biblioteca jSON;
- linhas 33-39: sem esta propriedade, surgem erros no momento da geração do binário APK do projeto;
1.16.2.3. O manifesto da aplicação Android
![]() |
O ficheiro [AndroidManifest.xml] tem de ser alterado. Com efeito, por predefinição, os acessos à Internet estão desativados. É necessário ativá-los através 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 de enviar uma 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] deste 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 de espera máximo para aguardar a resposta do servidor;
- linha 15: pretende-se definir um tempo de espera antes da execução do pedido ao servidor, para dar tempo ao utilizador para cancelar o seu pedido;
1.16.2.4.2. A interface [WebClient]
![]() |
A interface [WebClient] encarrega-se de comunicar com o serviço web. O 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 si própria, graças às anotações que nela iremos inserir. Esta 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 cadeia jSON, esta seja automaticamente deserializada. Assim, vemos na linha (d) que o URL [/{a}/{b}] devolve um tipo String, que é, na verdade, uma cadeia jSON (linha b). Com estas informações e as relativas ao tipo esperado na linha 16, a instância [WebClient] do cliente irá deserializar a cadeia que irá receber num 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 escrever-se 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. Do 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 um pedido HTTP POST e que o corpo desse pedido (objeto enviado) deve ser transmitido sob a forma de uma cadeia 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 de poder indicar o URL do serviço web a contactar. Isto é conseguido através da extensão da interface [RestClientRootUrl] fornecida por AA. Esta interface expõe um método [setRootUrl(urlServiceWeb] que permite definir o URL do serviço web a contactar;
- além disso, pretendemos controlar a chamada ao serviço web, pois queremos limitar o tempo de espera pela resposta. Para tal, estendemos a interface [RestClientSupport], que expõe o método [setRestTemplate], o que nos permitirá:
- criar nós próprios o objeto [RestTemplate], que serve para gerir 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] devolve uma resposta do tipo [Response] da seguinte forma:
package exemples.android.dao;
import java.util.List;
public class Response<T> {
// ----------------- propriedades
// estado da operação
private int status;
// eventuais mensagens de erro
private List<String> messages;
// o corpo da resposta
private T body;
// construtores
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters e setters
...
}
Trata-se da classe [Response] já utilizada no lado do servidor (parágrafo 1.16.1.5). De facto, do ponto de vista da programação, tudo acontece como se a camada [DAO] do cliente comunicasse diretamente com o controlador [WebController] do serviço web:
![]() |
A comunicação de rede entre o cliente e o servidor, bem como a serialização/desserialização dos objetos Java do 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 a transformar num bean AA que poderemos injetar noutro local;
- linhas 19-20: injetamos a implementação que será feita da interface [WebClient] que descrevemos. É a anotação [@RestService] que assegura esta injeção;
- os restantes 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 das solicitações do cliente REST
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
// constrói-se 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]. Esta 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 à comunicação com o serviço web. Passamos-lhe como parâmetro o objeto [factory] que acabou de ser criado;
- linha 10: o diálogo cliente/servidor pode assumir várias formas. As trocas de dados são feitas através de linhas de texto e temos de indicar ao objeto do tipo [RestTemplate] o que deve fazer com essa linha de texto. Para tal, fornecemos-lhe conversores, ou seja, classes capazes de processar as linhas de texto. A escolha do conversor é geralmente feita através 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 estes conversores: o AA obriga-nos a incluí-lo também 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, apesar de já o termos especificado por programação.
- Linha 12: o objeto [RestTemplate] assim construído é inserido na implementação da interface [WebClient] e é este objeto que irá estabelecer a comunicação 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) {
// retornamos o resultado
return info.getBody();
} else {
// registo da exceção
ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
}
} catch (JsonProcessingException | RuntimeException e) {
// regista-se a exceção
ex = new DaoException(e, 100);
}
// lança-se a exceção
throw ex;
}
...
// métodos privados -------------------
private void waitSomeTime(int delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- linha 8: aguarda-se [delay] milissegundos;
- linha 10: limita-se a 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 o seu [status];
- linhas 12-14: se não tiver ocorrido nenhum erro do lado do servidor (status=0), então devolvemos o resultado do método;
- linha 17: se tiver ocorrido um erro do lado do servidor (status!=0), prepara-se uma exceção sem a lançar. O servidor transmitiu uma lista de mensagens de erro. Criamos uma exceção com, como única mensagem, a cadeia jSON da lista de mensagens do servidor;
- linhas 19-22: outros casos de exceção;
- linha 24: quando se chega aqui, é inevitável que tenha ocorrido uma exceção. Por isso, lançamo-la;
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 através do método [setRootUrl] da interface [WebClient]. É porque esta interface estende a interface [RestClientRootUrl] que este método existe;
1.16.2.5. O pacote [architecture]
O pacote [architecture] agrupa os elementos que estruturam a aplicação:
![]() |
![]() |
1.16.2.5.1. A interface [IMainActivity]
A interface [IMainActivity] enumera os métodos que a atividade da aplicação deve implementar:
package exemples.android.architecture;
import exemples.android.dao.IDao;
public interface IMainActivity extends IDao {
// acesso à sessão
Session getSession();
// mudança de vista
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), adicionámos dois métodos para gerir a imagem de espera da aplicação (linhas 14, 16);
- linha 21: define-se um tempo máximo de espera pela resposta do servidor de 1 segundo;
1.16.2.5.2. A classe [Utils]
Reunimos na classe [Utils] métodos utilitários estáticos que podem ser chamados a partir de diferentes pontos da arquitetura da aplicação:
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) {
// construção do texto a apresentar
StringBuilder texte = new StringBuilder();
List<String> messages = getMessagesFromException(th);
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// resultado
return texte.toString();
}
}
- linhas 9-18: cria a lista de mensagens de erro contidas num Throwable;
- linhas 21-32: baseia-se no método anterior para construir, a partir da lista de mensagens obtida, o texto a apresentar numa mensagem de alerta do Android;
- linhas 27-28: as mensagens são numeradas. O número mais baixo (1) corresponde à exceção inicial e o número mais alto à 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 durante a exibição do fragmento e apenas uma vez;
- fatorizar o estado e os métodos das classes filhas que possam ser fatorizados;
É a função 2 que nos leva a colocar nesta classe as operações de gestão da imagem de espera: todos os fragmentos de uma aplicação Android assíncrona têm de lidar com este tipo de problema:
// gestão da espera
protected void beginWaiting() {
// ativa-se a ampulheta
mainActivity.beginWaiting();
}
protected void cancelWaiting() {
// retira-se a ampulheta
mainActivity.cancelWaiting();
}
1.16.2.6. A vista
![]() |
1.16.2.6.1. A vista [vue1.xml]
![]() |
Em comparação com o exemplo anterior, a vista [vue1.xml] apresenta as seguintes alterações:
![]() |
![]() |
- em [1], o utilizador 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 contabilizadas;
- em [4], o utilizador pode cancelar o seu pedido;
- em [5], é exibido um indicador de espera quando os números são solicitados. Este desaparece quando todos os números tiverem sido recebidos ou quando a operação tiver sido cancelada;

- em [6], verifica-se a validade dos dados introduzidos;
O utilizador é convidado a carregar o ficheiro [vue1.xml] a partir dos exemplos. A seguir, apresentamos o identificador dos novos componentes:

Os botões [10-11] estão fisicamente uns sobre os outros. 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 vista [vue1.xml] (linha 20);
- linhas 55-69: o método [@AfterViews] executado quando as referências das linhas 24-49 foram inicializadas;
- linha 58: a não esquecer — 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). Recorde-se que estão fisicamente um sobre o outro;
- linha 68: o campo da linha 52 irá conter a lista de cadeias de caracteres a apresentar pelo ListView das respostas;
Imediatamente a seguir ao método [@AfterViews], será executado o método [updateFragment] seguinte:
@Override
protected void updateFragment() {
// cria-se 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. Este é armazenado numa variável de instância para ficar disponível para os outros métodos da classe;
Ao clicar no botão [Exécuter], é executado o seguinte método:
// os dados introduzidos
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() {
// apagam-se as respostas anteriores
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// zera-se 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);
// solicitam-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) {
// deve-se fazer o mínimo possível aqui
// em qualquer caso, nenhuma exibição — estas devem ser feitas no UiThead
try {
// exibe-se o resultado no UiThread
showInfo(mainActivity.getAlea(a, b));
} catch (RuntimeException e) {
// a exceção é apresentada no UiThread
showAlert(e);
}
}
- linhas 17-18: apaga-se a lista anterior de respostas do servidor. Para tal, na linha 17, esvazia-se a fonte de dados [reponses] associada ao adaptador ListView;
- linha 19: um valor booleano que nos permitirá saber se o utilizador cancelou ou não o seu pedido;
- linhas 21-22: exibe-se um contador com valor zero para o número de respostas;
- linhas 24-26: recuperam-se os dados introduzidos nas linhas [2-6] e verifica-se a sua validade. Se algum deles for inválido, o método é abandonado (linha 25) e o utilizador é redirecionado para a interface visual;
- linhas 28-29: se todos os dados introduzidos 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). Estas informações são necessárias para a camada [DAO] e recorde-se que é a atividade que comunica com esta;
- linhas 31-33: os números aleatórios são solicitados um a 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 seja executado num outro thread (fluxo de execução, processo) diferente daquele em que a interface visual está a ser executada. De facto, é obrigatório executar qualquer chamada à Internet num thread diferente daquele da interface visual. Assim, num determinado momento, poderemos ter vários threads:
- aquele que apresenta a interface visual UI (User Interface) e gere os seus eventos,
- os threads [nbAleas], cada um dos quais solicita um número aleatório ao serviço web. Estas 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 a sua conclusão. Esta será-lhe sinalizada por um evento. Assim, as threads [nbAleas] serão iniciadas em paralelo. É possível configurar a aplicação para que inicie apenas uma thread de cada vez. Nesse caso, existe uma fila de espera das threads a executar;
Na linha 38, o parâmetro [id] atribui um nome ao thread gerado. Aqui, os threads [nbAleas] têm todos o mesmo nome: [alea]. Isto permitir-nos-á cancelá-los todos ao mesmo tempo. Este parâmetro é opcional se não se gerir o cancelamento do thread;
- linha 44: o método [getAlea] da atividade é chamado. Será, portanto, chamado num thread separado do do UI. Este último fará a chamada ao serviço web e não aguardará a resposta. Será notificado posteriormente, através de um evento, de 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 do pedido web pode gerar uma exceção. Solicita-se, então, a exibição das mensagens de erro da exceção numa mensagem de alerta;
- linha 35: entra-se em espera pelos resultados:
- será exibido um indicador de espera;
- o botão [Annuler] substituirá o botão [Exécuter]. Como os threads iniciados são assíncronos, o thread do UI não aguarda a sua conclusão e a linha 35 é executada antes do seu término. Assim que o método [beginWaiting] terminar, o UI pode voltar a responder às solicitações do utilizador, como, por exemplo, o clique no botão [Annuler]. Se os threads iniciados fossem síncronos, só se chegaria à linha 35 depois de todos os threads terem terminado. A anulação destes já não faria 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á terminámos?
if (nbInfos == nbAleas) {
// termina-se a espera
cancelWaiting();
}
// adiciona-se a informação à lista de respostas
reponses.add(0, String.valueOf(alea));
// exibimos as respostas
adapterReponses.notifyDataSetChanged();
}
}
- o método [showInfo] é chamado no interior do thread [getAlea], anotado por [@Background]. Este método irá atualizar a interface visual UI. Só o pode fazer se for executado no interior do thread UI. É este 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 utilizador não tiver cancelado o seu pedido;
- linhas 5-6: incrementa-se o contador de respostas e este é apresentado;
- linhas 8-11: se tiverem sido recebidas todas as respostas esperadas, então termina-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 apresentada pelo componente [ListView listReponses] e atualiza-se esta lista;
O método [showAlert] é o seguinte:
@UiThread
protected void showAlert(Throwable th) {
if (!hasBeenCanceled) {
// cancela-se tudo
doAnnuler();
// exibe-se
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
Encontramos aqui 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 só é executado se o utilizador não tiver cancelado o seu pedido;
- linha 5: anula-se o pedido do utilizador como se ele próprio tivesse clicado no botão [Annuler];
- linha 7: exibe-se o alerta utilizando 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 irá fechar a janela de alerta [3];
- [show]: solicita a exibição da janela de alerta;
![]() |
O «clique» no botão [Annuler] é gerido 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: regista-se que o utilizador cancelou o seu pedido;
- linha 6: cancela todas as tarefas identificadas pela cadeia [alea]. O segundo parâmetro [true] significa que estas devem ser canceladas mesmo que já tenham sido iniciadas. O identificador [alea] é o utilizado para qualificar o método [getAlea] do fragmento (linha 1 abaixo):
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
Nota: 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]. Com efeito, em caso de exceção (servidor ausente), a janela de alerta era apresentada n vezes se tivéssemos solicitado n números aleatórios.
1.16.2.7. A atividade [MainActivity]
![]() |
1.16.2.7.1. A vista [activity-main.xml]
![]() |
Em comparação com o exemplo anterior, adicionámos uma imagem de espera na vista 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, é-lhe injetada a camada [DAO]:
// injeção dao
@Bean(Dao.class)
protected IDao dao;
...
@AfterInject
protected void afterInject() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// configuração da camada [DAO]
setTimeout(TIMEOUT);
}
- linhas 2-3: injeção da camada [DAO] através de uma anotação AA;
- linhas 5-13: código executado após esta 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 vista de posição
if (mViewPager.getCurrentItem() != position) {
// exibição do fragmento
mViewPager.setCurrentItem(position);
}
}
// gestão 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 em [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 tiver instalado o [GenyMotion], a máquina virtual VirtualBox adicionou endereços IP ao seu computador (linhas 10 e 18). Estes endereços são particularmente práticos, pois não são bloqueados pela firewall do Windows. A linha 30 indica o endereço IP do seu computador numa rede local. Para utilizar este endereço, é normalmente necessário desativar o firewall do Windows. Se estiver ligado a uma rede Wi-Fi, utilize o endereço Wi-Fi e, também neste caso, desative o firewall, caso tenha um.
Teste a aplicação nos seguintes casos:
- 100 números aleatórios no intervalo [1000, 2000] sem tempo de espera;
- 2000 números aleatórios no intervalo [10000, 20000] sem tempo de espera e anule a espera antes do fim da geração;
- 5 números aleatórios no intervalo [100, 200] com um tempo de espera de 5000 ms e cancele a espera antes do fim da geração;
1.16.2.9. Gestão do cancelamento
Para acompanhar o que acontece quando o utilizador solicita a anulação 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 seguinte código:
// 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) {
// registo
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: indicamos o nome da classe;
- linhas 16-18: escrevemos um registo sempre que o método [getAlea] é chamado;
Além disso, no fragmento [Vue1Fragment], adicionamos os seguintes registos:
@UiThread
protected void showInfo(int alea) {
// registo
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
....
}
@UiThread
protected void showAlert(Throwable th) {
// registo
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
...
}
}
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// registo
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
...
}
Sempre que o fragmento [Vue1Fragment] recebe informação da camada [DAO], é emitido um registo. Além disso, quando o método [doAnnuler] é chamado, o evento é registado.
Teste 1
Solicitam-se 5 números, embora o servidor não tenha sido iniciado. Obtêm-se os seguintes registos:
- linhas 1-5: o método [getAlea] da classe [Dao] é chamado cinco vezes. Recorde-se que se trata de chamadas assíncronas efetuadas pelo fragmento [VueFragment] e que este não aguarda o resultado da sua chamada;
- linha 7: a primeira solicitação HTTP ocorreu e o fragmento [VueFragment] recebeu a sua primeira exceção;
- linha 8: solicita então a anulação de todas as solicitações;
- linhas 9-12: verifica-se, no entanto, que recebe as quatro exceções seguintes. Portanto, as solicitações assíncronas que estavam em espera foram todas 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 fim desse intervalo. Os registos são os seguintes:
- linhas 1-5: o método [getAlea] da classe [Dao] é chamado cinco vezes;
- linha 7: o utilizador solicitou o cancelamento das requisições;
- linha 8: verifica-se que o método [Vue1_Fragment] recebe 5 valores. Mais uma vez, todas as solicitações assíncronas que estavam em espera foram executadas;
Foi por isso que tivemos de gerir um valor booleano [hasBeenCanceled], para evitar apresentar qualquer coisa quando tinha sido solicitada uma anulação. No código da anulação:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// registo
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// memória
hasBeenCanceled = true;
// a tarefa assíncrona é 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 isto se deva ao facto de as tarefas assíncronas partilharem o mesmo método anotado [@Background]:
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
1.17. Exemplo 16: gerir a assincronia com RxAndroid
Propomos agora gerir a assincronia necessária às aplicações Android com uma biblioteca denominada RxJava [http://reactivex.io/] e a sua versão derivada para o ambiente Android, [RxAndroid]. Para tal, 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 ter o seguinte aspeto:
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 de enviar a solicitação
void setDelay(int delay);
// modo de depuração
void setDebugMode(boolean isDebugEnabled);
}
- linha 8: o método [getAlea] devolve agora 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 subscritores (assinantes, observadores, consumidores) do tipo Subscriber<T>. A biblioteca RxJava permite que o fluxo Observable<T> seja executado num thread T1 e o seu observador Subscriber<T> num thread T2, sem que o programadortenha de se preocupar com a gestão do ciclo de vida dessas threads nem com problemas naturalmente complexos, tais como a partilha de dados entre threads e a sincronização das mesmas para executar uma tarefa global. Facilita, assim, 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 do 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 {
// efetua-se a solicitação síncrona e a resposta é encaminhada ao assinante
Response<T> response = request.getResponse();
// erro?
int status = response.getStatus();
if (status != 0) {
// regista-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) {
// regista-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 pretendido 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-17. Esta interface possui apenas um método, [getResponse], e é este método que fornece a resposta desejada, [Response<T>];
- graças aos dois elementos anteriores, a classe [AbstractDao] pode servir de 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] devolve um tipo [Observable<T>] que representa o resultado efetivamente 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 deste 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:
- emite os seus elementos através do método [onNext(T element)];
- o método [onCompleted] deve ser chamado uma única vez assim que não houver mais elementos a enviar 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 recebe os elementos emitidos pelo [Observable<T>] (elemento ou exceção);
- o tipo [Observable<T>] criado emitirá apenas um único elemento (linha 37);
- linha 29: efetua uma consulta síncrona HTTP ao servidor e obtém o tipo [Response<T>]. Esta consulta HTTP é assegurada 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 este status corresponder a um erro, prepara-se uma exceção;
- linhas 36-39: se este status não corresponder a um erro, então emite-se a resposta realmente esperada pelo cliente (linha 37) e indica-se ao observador que não haverá mais emissões (linha 39);
- linhas 41-44: se a solicitação HTTP terminar numa exceção, esta é registada;
- linhas 46-49: se a exceção [ex] for diferente de null, então envia-se essa exceção para o observador. Não é necessário chamar o método [onCompleted] para indicar ao observador que não haverá mais emissões de elementos. Isso está implícito;
O que se retira destas explicações é que:
- o método genérico [<T> Observable<T> getResponse(final IRequest<T> request)] devolve um tipo [Observable<T>] que emite apenas um elemento do tipo T ou uma exceção;
- que este método admite como único parâmetro um tipo [IRequest<T>], cujo único método [getResponse()] realiza o acesso HTTP, que devolve 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 do pedido
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) {
// registo
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() {
// em 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] passa agora a devolver um tipo [Observable<Integer>];
- linha 30: chamada do método genérico [getResponse] da classe pai. É-lhe passado um parâmetro do tipo [IRequest<Integer>];
- linhas 32-37: implementação da interface [IRequest<Integer>];
- linha 36: efetua-se a consulta HTTP através da interface AA [webClient], tal como tinha sido feito anteriormente. Sabemos que iremos recuperar um tipo [Response<Integer>], que é precisamente o tipo que o método [IRequest<Integer>.getReponse()] deve devolver;
- linha 36: aqui utiliza-se uma propriedade denominada closure: a capacidade de encapsular numa instância valores externos à mesma no momento da sua criação, neste caso os valores de [a, b] da linha 24. É isto 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>] que encapsula 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] evolui da seguinte forma:
@Click(R.id.btn_Executer)
protected void doExecuter() {
// apagam-se as respostas anteriores
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// o contador de respostas é repostado a 0
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// verifica-se a validade dos dados introduzidos
if (!isPageValid()) {
return;
}
// inicialização da atividade
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// solicitam-se os números aleatórios
getAleasInBackground(a, b);
// inicia-se a espera
beginWaiting();
}
- linha 18: solicitam-se os números aleatórios ao método [getAleasInBackground], assim denominado porque os números serão solicitados num thread diferente do da interface do utilizador;
private int nbReponses = 0;
// as subscrições dos observáveis
private List<Subscription> abonnements;
// anotação [Background] desnecessária
void getAleasInBackground(int a, int b) {
// Inicialmente, sem respostas nem subscrições
nbReponses = 0;
abonnements.clear();
// prepara-se o observável
Observable<Integer> response = Observable.empty();
// estamos a fundir os resultados das diferentes chamadas HTTP
// são executados num 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 {
// executa-se o observável
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 é apresentada no UiThread
showAlert(e);
}
}
- linha 3: um observável tem subscritores. A ligação entre um subscritor e o processo que este observa denomina-se subscrição (Subscription). Aqui, teremos apenas um processo observado e um subscritor. Por conseguinte, teremos apenas uma subscrição. Por princípio, agimos como se pudéssemos ter vários processos observados por diferentes observadores, o que resultaria em várias subscrições;
- linhas 11-18: configura-se o processo observado (observável). É importante compreender 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 este observável vazio, adicionam-se os observáveis [nbAleas], que serão as requisições [nbAleas] que irão devolver números aleatórios [nbAleas];
- linha 15: tal como anteriormente, o número aleatório n.º i é solicitado à classe [MainActivity]. É importante compreender que, nesta fase, ainda não foi executada nenhuma consulta HTTP. O método [mainActivity.getAlea(a, b)] é executado e devolve um tipo [Observable<Integer>]. Trata-se de um processo que será observado quando for iniciado;
- linha 15: o método [subscribeOn(Schedulers.io())] solicita que o processo seja executado (quando tal acontecer) num thread de E/S. A biblioteca RxJava disponibiliza diferentes tipos de threads. O de E/S é adequado para as chamadas HTTP;
- linha 15: o observável n.º i é fundido com o observável inicial da linha 11: a partir de observáveis [nbAleas], cada um dos quais emite um elemento, cria-se um observável que emitirá [nbAleas] elementos. É este que será observado. Este observável emite a notificação [onCompleted] quando todos os observáveis que o compõem tiverem emitido a sua própria notificação [onCompleted]. Isto evitar-nos-á ter de contar as respostas, como fizemos na versão anterior, para saber se recebemos todos os números esperados;
- linha 18: quando chegamos aqui, já configurámos um observável que é a composição de [nbAleas] observáveis, cada um a ser executado num thread de E/S;
- linha 18: o método [observeOn(AndroidSchedulers.mainThread())] serve para indicar em que thread deve ser feita a observação dos valores emitidos pelo observável. Aqui, o thread [AndroidSchedulers.mainThread())] pertence à biblioteca RxAndroid e não à RxJava. Designa o thread da interface do utilizador, também denominado «event loop». Este ponto é importante: numa aplicação Android, a modificação de um componente da interface do utilizador só pode ser efetuada no thread da interface do utilizador; caso contrário, ocorre uma exceção;
- linhas 19-45: agora que o processo a observar foi configurado, executa-se o mesmo;
- linha 21: é a operação [Observable.subscribe] que inicia a execução do processo observado. Esta operação irá iniciar os processos assíncronos [nbAleas] configurados anteriormente. Os resultados destes serão automaticamente disponibilizados ao observador na thread da IU;
- lembramo-nos de que o observável emite três tipos de eventos:
- [onNext]: quando emite um elemento;
- [onError]: quando encontra uma exceção;
- [onCompleted]: quando sinaliza que não irá 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 destes três eventos;
- linhas 21-27: o primeiro parâmetro do tipo [Action1<Integer>] serve para tratar o evento [onNext]. O 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]. O 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 do pedido do utilizador. Isto consistirá em cancelar todos os observáveis que se encontram em execução;
- linhas 35-41: o terceiro parâmetro do tipo [Action0] serve para tratar o evento [onCompleted]. O 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) {
// registo
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));
// adiciona-se a informação à lista de respostas
reponses.add(0, String.valueOf(alea));
// exibem-se as respostas
adapterReponses.notifyDataSetChanged();
}
}
O método apresenta duas alterações:
- linha 1: foi removida a anotação AA [@UiThread];
- já não se contam as respostas para determinar se se deve ou não interromper a espera. Agora, é o evento [onCompleted] do observável que nos fornece essa informação;
O método [showAlert] evolui da seguinte forma:
// anotação [UiThread] desnecessária
protected void showAlert(Throwable th) {
// registo
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
if (!hasBeenCanceled) {
// cancela-se tudo
doAnnuler();
// está no cartaz
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
- a única alteração ocorre 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() {
// registo
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// memória
hasBeenCanceled = true;
// cancela-se as tarefas assíncronas
if (abonnements != null) {
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
}
// fim da espera
cancelWaiting();
}
- linha 12: cancela uma subscrição 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 realizou com o exemplo anterior (parágrafo 1.16.2.8).
1.17.10. Gestão do cancelamento
Repetimos os mesmos testes que no exemplo anterior (parágrafo 1.16.2.9).
Teste 1
Solicitam-se 5 números, embora o servidor não tenha sido iniciado. Obtêm-se os seguintes registos:
Após a linha 7, não há mais registos, o que demonstra que o observador (Vue1Fragment) já não recebe notificações do processo observado.
Teste 2
Agora, inicie o servidor e solicite 5 números com um intervalo de 5 segundos e clique em [Annuler] antes do fim desse intervalo. Os registos são os seguintes:
Após a linha 6, não há mais registos, o que demonstra que o observador (Vue1Fragment) já não recebe notificações do processo observado.
Este é o comportamento esperado de uma anulação. Podemos, portanto, remover do código de [Vue1Fragment] a variável booleana [hasBeenCanceled] que tínhamos introduzido no exemplo anterior, uma vez que a anulação não estava a funcionar como esperado.
O facto de o observador já não receber notificações após a anulação do observável não significa que as solicitações HTTP sejam, elas próprias, anuladas. Para o verificar, alteramos a classe [Dao] da seguinte forma:
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// registo
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: registamos o resultado da consulta HTTP da linha 14;
Os registos para o teste n.º 2 são, então, os seguintes:
- linhas 1-5: as 5 solicitações foram efetuadas;
- linha 6: o utilizador cancelou;
- linhas 7-11: recebem-se corretamente as respostas das cinco consultas HTTP. No entanto, devido ao cancelamento do observável, estes 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, pelas seguintes razões:
- A RxAndroid pode ser utilizada numa aplicação Android que não utilize a AA;
- A RxAndroid faz mais do que facilitar as operações assíncronas. Oferece inúmeros métodos para criar um novo observável a partir de outro. Estes métodos não têm equivalente na AA;
- assim que se pretende derivar uma classe anotada por AA, como um fragmento, deparam-se com sérios problemas. É então 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 introdução de dados
Vamos criar um novo projeto para apresentar alguns componentes comuns em formulários de introdução de dados.
1.18.1. Criação do projeto
Duplicamos o projeto [Exemple-13] para [Exemple-17]:
![]() | ![]() |
O novo projeto terá apenas uma vista, [vue1.xml]. Por isso, eliminamos a vista [vue2.xml] e os seus fragmentos associados [Vue2Fragment] e [2]. Temos em conta esta alteração no gestor de fragmentos de [Mainactivity]:
// o nosso gestor de fragmentos deve ser redefinido para cada aplicação
// deve definir os seguintes métodos: getItem, getCount, getPageTitle
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// os fragmentos
private final Fragment[] fragments = {new Vue1Fragment_()};
....
}
Execute novamente o projeto. Deve aparecer a vista n.º 1, tal como anteriormente. Vamos trabalhar a partir deste projeto.
1.18.2. A vista XML do formulário
![]() |
A vista gerada pelo ficheiro [vue1.xml] é a seguinte:

O texto XML da vista é 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 cadeias de caracteres do formulário
As cadeias de caracteres do formulário estão definidas no seguinte ficheiro [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>
<!-- vista 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 vista apresentada por um contentor de fragmentos
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// os campos da vista apresentada 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 selecionar um botão de opção ou uma caixa de seleção;
- linha 60: por predefinição, o componente [DatePicker] apresenta um campo de introdução de data e um calendário. A linha 60 elimina 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: gerem-se os eventos da barra de ajuste. Pretende-se, a cada alteração efetuada pelo utilizador, apresentar o valor da régua no [TextView] da linha 49;
- linha 71: o parâmetro [progress] representa o valor da barra de ajuste;
- linhas 76-79: uma lista de [String] que iremos associar à 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 apresentar os valores introduzidos pelo utilizador. O seu código é o seguinte:
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// lista de mensagens a apresentar
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 introdução
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 multilinha
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 introduzidos serão acumulados numa lista de mensagens;
- linha 6: o método [CheckBox].isCkecked() permite saber 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 selecionado e, assim, obter o 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 com 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 numa lista suspensa;
- linha 35: o método [Spinner].getSelectedItem() permite obter o objeto selecionado numa lista suspensa;
O método [doAfficher], que apresenta a lista dos valores introduzidos, é o seguinte:
private void doAfficher(List<String> messages) {
// construímos o texto a exibir
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// exibe-se
new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
- linha 1: o método recebe uma lista de mensagens a apresentar;
- linhas 3-6: é criado um objeto [StringBuilder] 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 apresenta o texto da linha 3:

1.18.5. Execução do projeto
Execute o projeto e teste os diferentes componentes de introdução de dados.
1.19. Exemplo 18: utilização de um modelo de vistas
1.19.1. Criação do projeto
Criamos um novo projeto [Exemple-18] a partir do projeto [Exemple-13].
![]() | ![]() |
1.19.2. O modelo de vistas
Queremos recuperar as duas vistas do projeto e incluí-las num modelo:
![]() |

Cada uma das duas vistas será estruturada da mesma forma:
- em [1], um cabeçalho;
- em [2], uma coluna à esquerda que poderá conter links;
- em [3], um rodapé;
- em [4], um conteúdo.
Isto é conseguido alterando a vista de base [activity_main.xml] da atividade;
![]() | ![]() |
O código XML da vista [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 a partir das linhas 56 a 84;
- o rodapé [3] é obtido a partir das linhas 86 a 101;
- o conteúdo [4] é obtido a partir das linhas 78 a 84;
A vista XML [main] utiliza informações encontradas nos ficheiros [res / values / colors.xml] e [res / values / strings.xml]:
![]() |
O ficheiro [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 ficheiro [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 vista específica para cada elemento de uma lista. A vista repetida pode ter qualquer nível de complexidade, desde uma simples cadeia de caracteres até uma vista que permita introduzir informações para cada elemento da lista. Vamos criar o seguinte [ListView]:

Cada vista da lista tem três componentes:
- um [TextView] de informação;
- um [CheckBox];
- um [TextView] clicável;
1.20.1. Criação do projeto
Criamos um novo projeto [Exemple-19] através da cópia do projeto [Exemple-18].
![]() | ![]() |
![]() |
Vamos desenvolver o projeto conforme indicado em [3].
1.20.2. A sessão
![]() |
A sessão armazena os dados partilhados 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 vistas;
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] passa a ser o seguinte:
// injeção de sessão
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// registo
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 vista inicial [Vue1]
![]() | ![]() |
A vista XML [vue1.xml] apresenta a área [1] acima referida. O 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 vista repetida pelo [ListView]
![]() |
A vista repetida pelo [ListView] é a seguinte vista [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] gere a vista XML [vue1]. O 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 vista apresentada 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 vista 2
mainActivity.navigateToView(1);
}
public void doRetirer(int position) {
...
}
@Override
protected void updateFragment() {
if (!initDone) {
// associam-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 — neste caso, é necessário voltar a ligar o ListView ao seu adaptador
listView.setAdapter(adapter);
// caso em que outros fragmentos tenham alterado a fonte de dados — neste caso, é necessário atualizar o ListView
adapter.notifyDataSetChanged();
}
}
- linha 15: a vista XML [vue1] está associada ao fragmento;
- linhas 26-30: o método [@AfterViews] não faz nada. No entanto, é necessário para definir a variável [afterViewsDone] como true, uma vez que 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, assim, reiniciar o seu ciclo de vida. Não é esse o caso aqui, mas seria se a aplicação viesse a ter 3 fragmentos com uma adjacência de 1;
- linha 44: o adaptador do [ListView] só precisa de ser inicializado uma vez;
- linha 46: associamos a este [ListView] um adaptador do tipo [ListAdapter]. Vamos criar esta classe. Ela deriva da classe [ArrayAdapter], que já tivemos oportunidade de utilizar para associar dados a um [ListView]. Passamos várias informações ao construtor do [ListAdapter]:
- uma referência à atividade atual,
- o identificador da vista que será instanciada para cada elemento da lista,
- uma fonte de dados para alimentar a lista,
- uma referência ao fragmento. Esta será utilizada para gerir o clique num link [Retirer] do [ListView] através 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]. Esta operação será aqui realizada sempre que a vista n.º 1 for apresentada. Na realidade, só precisaria de ser realizada quando o método [@AfterViews] tivesse sido executado. Aqui, a instrução é executada com demasiada frequência. Sente-se a necessidade de uma variável booleana que nos indique que o método [@AfterViews] acabou de ser executado e que, por isso, 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 vista n.º 1 pode alterar a fonte de dados do [ListView]. Consideremos um caso mais geral em que a vista n.º 2 também poderia alterar a fonte de dados do [ListView]. Encontraremos exemplos deste tipo mais adiante neste documento. Neste caso, quando se passa da vista n.º 2 para a vista n.º 1, o [ListView] da vista n.º 1 deve ser atualizado;
1.20.7. O adaptador [ListAdapter] do [ListView]
![]() |
A classe [ListAdapter]
- configura a fonte de dados do [ListView];
- gere a apresentação dos diferentes elementos do [ListView];
- gere os eventos desses elementos;
O 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 visualização de uma linha da lista
private int layoutResourceId;
// os dados da lista
private List<Data> data;
// o fragmento que apresenta 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 guardadas
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: guardam-se as informações do construtor;
- linha 29: o método [getView] será chamado repetidamente pelo [ListView] para gerar a vista do elemento n.º [position]. O resultado [View] obtido é uma referência à vista 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);
}
});
// gestão do 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 utilizar apenas o primeiro;
- linha 4: cria-se a vista do elemento n.º [position]. Trata-se da vista [list_data], cujo ID foi passado como segundo parâmetro ao construtor. Em seguida, recuperam-se as referências dos componentes da vista que acabámos de instanciar;
- linha 6: recuperamos a referência do [TextView] n.º 1;
- linha 7: atribui-se-lhe 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: gere-se o clique no link [Retirer];
- linha 16: é o método [Vue1Fragment].doRetirer que irá gerir este clique. De facto, parece mais lógico que este evento seja gerido pelo fragmento que apresenta o [ListView]. Este possui uma visão global que a classe [ListAdapter] não tem. A referência do fragmento [Vue1Fragment] tinha sido passada como quarto parâmetro ao construtor da classe;
- linhas 20-25: trata-se do clique na caixa de seleção. A ação realizada sobre ela é refletida nos dados que esta apresenta. Isto deve-se ao seguinte motivo. O [ListView] é uma lista que apresenta apenas uma parte dos seus elementos. Assim, um elemento da lista está, por vezes, oculto e, por vezes, visível. 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 irá recalcular o estado da caixa de seleção com base nos dados aos quais está associada. É, portanto, necessário que esta 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);
// regista-se a posição do scroll 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 na totalidade ou não
int firstPosition = listView.getFirstVisiblePosition();
// desvio no eixo Y deste 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] em que se clicou;
- linha 3: recupera-se a lista de dados;
- linha 4: remove-se o elemento com o n.º [position];
- linha 15: atualiza-se o [ListView]. Sem isso, visualmente nada muda.
- linhas 5-13, 17: um processo bastante complexo. Sem ele, acontece o seguinte:
- o [ListView] apresenta as linhas 15-18 da lista de dados,
- elimina-se a linha 16,
- a linha 15 acima reinicia-o totalmente e o [ListView] apresenta então as linhas 0-3 da lista de dados;
Com as linhas acima, a eliminação é efetuada e o [ListView] permanece posicionado na linha seguinte à linha eliminada.
1.20.9. A vista XML [Vue2]
![]() | ![]() |
O código XML da vista é 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] gere a vista XML [vue2]. O 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 vista
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@AfterViews
void initFragment(){
// memória
afterViewsDone=true;
}
@Click(R.id.button_vue1)
void navigateToView1() {
// navegamos para a vista 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// são apresentados os elementos da lista que foram selecionados na vista 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 encontra-se no método [updateFragment], na linha 32:
- linha 34: calcula-se o texto a apresentar no [TextView] n.º 2;
- linhas 35-39: percorre-se a lista de dados apresentada pelo [ListView]. Esta está armazenada na atividade;
- linha 36: se o dado n.º i tiver sido assinalado, adiciona-se a descrição associada num tipo [StringBuilder];
- linha 41: o [TextView] apresenta 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, utilizámos 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, utilizámos um valor booleano para gerir a caixa de seleção dos elementos do [ListView]. Frequentemente, o [ListView] tem de apresentar dados que podem ser selecionados marcando uma caixa de seleção, sem que o elemento da fonte de dados tenha um campo booleano correspondente a essa caixa. Nesse caso, pode-se proceder da seguinte forma:
A classe [Data] passa a ter a seguinte forma:
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
...
}
Basta, em seguida, substituir em todo o código (MainActivity, ListAdapter, Vue1Fragment, Vue2Fragment), o tipo [Data] pelo tipo [CheckedData]. Por exemplo, em [MainActivity]:
@AfterInject
protected void afterInject() {
// registo
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 é-lhe fornecido com o nome [Exemple-19B].
1.21. Exemplo 20: utilizar um menu
1.21.1. Criação do projeto
Duplicamos o projeto [Exemple-19B] no projeto [Exemple-20]:
![]() | ![]() |
![]() | 3 ![]() |
Vamos eliminar os botões das vistas 1 e 2 para os substituir por opções de menu [1-2].
1.21.2. A definição XML dos menus
![]() |
O ficheiro [res / menu / menu_vue1] define o menu da vista 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 elementos do menu são definidos pelas seguintes informações:
- android:id: o identificador do elemento;
- android:title: o título do elemento;
- app:showsAsAction: indica se o elemento do menu pode ser colocado na barra de ações da atividade. [ifRoom] indica que o elemento 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 (etiqueta <menu>, linhas 25, 29);
O ficheiro [res / menu / menu_vue2] define o menu da vista 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. A gestão do menu na classe abstrata [AbstractFragment]
Vamos factorizar a gestão do menu na classe pai [AbstractFragment] das duas vistas:
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();
// registo
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// memória
this.menu = menu;
// registo
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
// recuperam-se as # opções do menu, caso ainda não tenha sido feito
if (!initDone) {
// recuperam-se as # opções do menu
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// transfere-se a lista de opções para um array
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 ao fragmento filho que se inicie
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) {
...
}
// atualizar classe filha
protected abstract void updateFragment();
}
- linha 42: os registos mostram que o método [onCreateOptionsMenu] é chamado sempre que o fragmento é apresentado. É chamado muito tarde, nomeadamente depois de o método [updateFragment] ter sido chamado. Isto sugere que poderia ser utilizado para atualizar o fragmento. É isso que vamos fazer 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 da sua descrição inicial. Não utilizaremos esta possibilidade aqui, pois utilizaremos uma anotação AA que o fará por nós;
- linha 44: guardamos o menu. Vamos precisar dele mais tarde;
- linhas 52-53: armazenamos na tabela da linha 28 os identificadores de todos os elementos do menu;
- linhas 55-57: os registos mostram que, quando o método [onCreateOptionsMenu] é chamado, o método [Fragment.getActivity()] devolve 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: guardamos a sessão;
- linha 59: observamos que a inicialização da classe já foi efetuada para não ser necessário repeti-la (linha 50);
- linha 63: solicitamos que o fragmento filho se atualize. Isto é possível porque o fragmento está simultaneamente visível e associado à sua vista 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-se
if (menuItem.hasSubMenu()) {
// recursividade
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
O método [setAllMenuOptions] permite ocultar/mostrar todas as opções do menu;
protected void setAllMenuOptions(boolean isVisible) {
// atualizam-se todas as opções do menu
for (int menuItemId : menuOptions) {
menu.findItem(menuItemId).setVisible(isVisible);
}
}
O método [setMenuOptions] permite ocultar/mostrar algumas das opções do menu;
protected void setMenuOptions(MenuItemState[] menuItemStates) {
// atualizam-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. A gestão 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 vista 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() {
// alteração 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: mostra-se/oculta-se a opção raiz [menuNavigation, menuActions];
- linha 40: para mostrar uma opção de um ramo do menu, é necessário não só mostrar essa opção, mas também todas as opções que se encontram ao subir da opção folha até à raiz do menu;
1.21.5. A gestão do menu no fragmento [Vue2Fragment]
Encontramos um código semelhante no fragmento da vista 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 vista
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@OptionsItem(R.id.navigationVue1)
void navigateToView1() {
// navega-se para a vista 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// exibem-se os elementos da lista que foram selecionados na vista 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 mostrou-nos que, quando o fragmento tem um menu, o seu método [onCreateOptionsMenu] é um bom local para solicitar que o fragmento se atualize:
- é chamado exatamente uma vez quando o fragmento vai ser apresentado;
- quando é chamado, são estabelecidas as associações do fragmento com a sua atividade, a sua vista e o seu menu;
Para demonstrar isto, retomamos o exemplo 12, que se caracteriza por ter muitos fragmentos cuja adjacência pode ser alterada. Neste exemplo, os fragmentos não tinham menu. Vamos associar-lhes 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 é importante compreender aqui é que a atividade já tem o 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á tem um menu, o menu associado aos fragmentos é adicionado ao da atividade: ficamos, assim, com as opções de dois menus. Neste caso, o menu dos fragmentos estará vazio. Por isso, só veremos 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], eliminamos as referências à antiga classe abstrata [AbstractFragment].
1.22.4. Execução
Execute a aplicação e verifique se funciona. Acompanhe os registos para ver quando é executado o método [onCreateOptionsMenu] da classe [AbstractFragment]. Agora é este método que chama o método [updateFragment] dos fragmentos filhos.
1.23. Exemplo 22: gravação/restauração do estado da atividade e dos fragmentos
1.23.1. O problema
Abordamos aqui o problema da rotação do dispositivo Android (vertical <--> horizontal). Para ilustrar, retomamos o exemplo 21 anterior:

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

Podemos ver que:
- em [1], o separador [Fragment n° 3] desapareceu;
- em [2], o texto apresentado é, de facto, o do fragmento n.º 3, mas o contador de visitas está incorreto;
Durante esta rotação, os registos são os seguintes:
- linha 1: verifica-se que a atividade foi totalmente reconstruída;
- linhas 3-7: o mesmo se aplica aos cinco fragmentos geridos pela atividade;
- linha 21: o fragmento n.º 3 vai ser apresentado. Vê-se que, antes do incremento, o número da visita é 0;
Pode-se então explicar o resultado obtido após a rotação da seguinte forma:
- a classe [MainActivity] cria inicialmente uma barra de separadores com um único separador, intitulado [Vue 1]. É este o separador que se vê;
- após a rotação do dispositivo, o gestor de páginas [mViewPager] volta a apresentar o mesmo fragmento, ou seja, neste caso, o fragmento n.º 3. É importante lembrar que separadores e fragmentos são conceitos diferentes e têm um ciclo de vida distinto. O método [updateFragment] do fragmento n.º 3 será executado:
public void updateFragment() {
// registo
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// incrementar o n.º 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, esta, tal como tudo o resto, foi reconstruída e o número da visita foi repostado a zero. O que explica o resultado apresentado no fragmento n.º 3;
1.23.2. Métodos de gravação/restauração da atividade e dos fragmentos
1.23.2.1. Solução 1: gravação manual
Durante a rotação do dispositivo, são chamados dois métodos da atividade:
// gestão de cópia de segurança / restauração da atividade ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// pai
super.onSaveInstanceState(outState);
// gravação 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. É aqui que se pode efetuar o backup da atividade. Se não se fizer nada, nada é guardado. O estado da atividade deve ser guardado no parâmetro [Bundle outState] passado ao método. A classe [Bundle] assemelha-se a um dicionário. Possui métodos [putString, putInt, putLong, putBoolean, putChar, ...] com dois parâmetros: void putT(String key, T value);
- linhas 10-16: o método [onCreate] é chamado aquando da criação da atividade. Se o estado desta tiver sido guardado, esse estado é-lhe passado no parâmetro [Bundle savedInstanceState]. Para recuperar os valores guardados, existem métodos como o [getString, getInt, getLong, geBoolean, getChar, ...] com um parâmetro: T getT(String key);
Os fragmentos dispõem destes mesmos dois métodos para guardar o seu estado.
Vamos utilizar estas informações para guardar e restaurar o estado do exemplo 21. Para tal, duplicamos o projeto [Exemple-21] em [Exemple-22].
1.23.2.2. Solução 2: gravação automática
A documentação do Android indica que, durante a rotação do dispositivo, é possível evitar a destruição de um fragmento utilizando a instrução: [Fragment].setRetainInstance(true). Vários artigos sobre [StackOverflow] recomendam utilizar esta 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 esta instrução em dois exemplos: Exemplo-17 (parágrafo 1.18 — uma aplicação com um fragmento que apresenta um formulário) e Exemplo-21 (parágrafo 1.22), uma aplicação com cinco fragmentos. Em ambos os casos, esta única instrução aplicada a todos os fragmentos da aplicação revelou-se insuficiente para restaurar corretamente a vista apresentada 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 por predefinição, decidi seguir as recomendações de [StackOverflow] e manter o valor false como valor por defeito 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 gravação/restauração do projeto [Exemple-22]
O projeto [Exemple-22] evolui da seguinte forma:
![]() |
Surgem aqui duas novas classes:
- [PlaceHolderFragmentState], que armazenará o estado de um fragmento do tipo [PlaceHolderFragment];
- [Vue1FragmentState], que armazenará o estado do fragmento do tipo [Vue1Fragment];
Estas classes são as seguintes:
package exemples.android;
public class Vue1FragmentState {
// estado Vue1Fragment
private boolean hasBeenVisited=false;
// getters e setters
...
}
- linha 5: o valor booleano [hasBeenVisited] é verdadeiro se o fragmento [Vue1Fragment] tiver sido visitado (exibido) pelo menos uma vez. Este campo foi criado para o exemplo, uma vez que o fragmento [Vue1Fragment] não tem nada para guardar;
A classe [PlaceHolderFragmentState] é a seguinte:
package exemples.android;
public class PlaceHolderFragmentState {
// estado visitado ou não
private boolean hasBeenVisited;
// texto apresentado
private String text;
// getters e setters
...
}
- linha 5: encontramos o valor booleano [hasBeenVisited];
- linha 7: o texto apresentado pelo fragmento no momento em que deve ser guardado. Vimos que este texto se perdeu durante a rotação;
O estado dos fragmentos será armazenado na sessão e caberá à atividade guardar/restaurar essa sessão. A sessão evolui da seguinte forma:
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] apresentado no segundo separador
private int numFragment = -1;
// n.º do separador selecionado
private int selectedTab = 0;
// n.º da vista atual
private int currentView;
// fragmentos guardados ---------------
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.º do último separador selecionado;
- linha 15: o número do último fragmento exibido;
A atividade guarda/restaura a sessão da seguinte forma:
// gestão de gravação/restauração da atividade ----------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// pai
super.onSaveInstanceState(outState);
// salvaguarda da sessão
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// registo
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
@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();
}
// registo
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 é guardada sob a forma da sua cadeia jSON;
- linha 29: a sessão é restaurada a partir da sua cadeia jSON;
Para gerir o armazenamento e a restauração dos fragmentos, a classe abstrata [AbstractFragment] é alterada da seguinte forma:
// gestão de gravação/restauração -----------------------------------------------
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// pai
super.setUserVisibleHint(isVisibleToUser);
// cópia de segurança?
if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
// o fragmento vai ser ocultado - vamos guardá-lo
saveFragment();
saveFragmentDone = true;
}
// memória
this.isVisibleToUser = isVisibleToUser;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// pai
super.onActivityCreated(savedInstanceState);
// registo
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// o fragmento deve ser restaurado
fragmentHasToBeInitialized = true;
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// registo
if (isDebugEnabled) {
Log.d(className, "onSaveInstanceState");
}
// pai
super.onSaveInstanceState(outState);
// guardar o fragmento apenas se estiver visível
if (isVisibleToUser && !saveFragmentDone) {
saveFragment();
saveFragmentDone = true;
}
}
// classes filhas
protected abstract void updateFragment();
protected abstract void saveFragment();
- decide-se guardar 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 efetuar um registo do fragmento e este se encontra visível (linha 38);
Este mecanismo evita que se façam gravações com mais frequência do que o necessário. Com efeito, como se guardou o estado do fragmento i quando este passou de visível para oculto, quando o fragmento j é exibido e se efetua uma rotação, é desnecessário voltar a guardar o fragmento i. Se este não tiver sido exibido novamente desde a sua última gravação, então o seu estado não se alterou. Apenas o estado do fragmento j deve ser guardado. Este mecanismo tem ainda outra vantagem: não é apenas durante uma rotação do dispositivo que é necessário guardar o estado de um fragmento. Existe também o caso da navegação pura entre fragmentos, por exemplo, num sistema com separadores. Nesse caso, pretende-se recuperar um fragmento no estado em que foi deixado da última vez que foi exibido. Esse estado pode ter desaparecido parcialmente se esse fragmento tiver saído, a dada altura, da adjacência dos fragmentos exibidos. O fragmento não é, então, reconstruído na sua totalidade, mas a sua vista associada sim. O registo que foi feito quando o fragmento ficou oculto servirá para recuperar o último estado dessa vista;
- linhas 10, 40: para evitar efetuar dois registos sucessivos, utiliza-se o valor booleano [saveFragmentDone] para indicar que foi efetuado um registo;
- linhas 9, 39: solicita-se ao fragmento filho que guarde o 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. Com efeito, o fragmento filho deve saber que tem de 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;
// registo
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
...
// solicita-se que o fragmento filho se atualize
updateFragment();
// salvaguarda a efetuar
saveFragmentDone = false;
}
- linha 14: vimos que o valor booleano [saveFragmentDone] passou para vrai quando foi efetuado um salvamento. Em determinado momento, tem de voltar a faux. Quando o método [updateFragment] (linha 12) do fragmento filho for executado, este tornar-se-á visível. Ora, é quando está visível que um fragmento deve ser guardado, no momento específico em que passar do estado visível para o estado oculto. Assim, define-se o valor booleano [saveFragmentDone] para false para que o salvamento possa ocorrer;
1.23.4. Gravação do fragmento [Vue1Fragment]
O salvamento dos fragmentos é efetuado no método [saveFragment], chamado pela classe-pai [AbstractFragment]:
// gravação do estado do fragmento
@Override
public void saveFragment() {
// registo
if (isDebugEnabled) {
Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
}
// gravação do estado do fragmento na sessão
Vue1FragmentState state = new Vue1FragmentState();
state.setHasBeenVisited(true);
session.setVue1FragmentState(state);
// registo
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- linhas 9-11: gravação do estado do fragmento na sessão. Quando o método [saveFragment] é chamado, o fragmento fica visível. Por isso, é necessário definir o valor booleano [hasBeenVisited] como vrai (linha 10);
1.23.5. Gravação do fragmento [PlaceHolderFragment]
O armazenamento dos fragmentos é efetuado no método [saveFragment], chamado pela classe-pai [AbstractFragment]:
@Override
public void saveFragment() {
// o estado do fragmento está a ser guardado na sessão
PlaceHolderFragmentState state = new PlaceHolderFragmentState();
state.setText(textViewInfo.getText().toString());
state.setHasBeenVisited(true);
session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
// registo
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- linhas 4-7: gravação do estado do fragmento na sessão;
- linha 5: o texto atualmente exibido pelo [TextView] textViewInfo é guardado;
- linha 6: o valor booleano [hasBeenVisited] do fragmento é alterado para vrai;
- linha 7: o estado do fragmento é registado na tabela [placeHolderFragmentStates]. O número do elemento a inicializar é o número da secção do fragmento menos um;
1.23.6. Restauração do fragmento [Vue1Fragment]
A restauração dos fragmentos é efetuada no método [updateFragment]:
@Override
protected void updateFragment() {
// registo
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;
}
// registo
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.º de visita
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// exibir o n.º da visita
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
// alteração do n.º da vista 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 o seu valor é vrai, o fragmento acaba de ser reconstruído e é necessário reinicializá-lo. É aqui que isso acontece. Neste exemplo específico, não há nada a fazer. Mostrámos simplesmente que era possível recuperar o valor da variável booleana [hasBeenVisited] no estado guardado do fragmento (linha 10);
- linha 11: não se deve esquecer de voltar a definir o [fragmentHasToBeInitialized] para faux, para que, quando voltarmos posteriormente a este fragmento sem que tenha havido rotação do dispositivo, não se volte a efetuar uma inicialização desnecessária do fragmento;
- linhas 18-26: incremento do contador de visitas. Aqui, há uma dificuldade: quando se restaura o fragmento, não se pretende incrementar este contador. É necessário distinguir aqui entre:
- uma simples navegação que leva o utilizador de volta ao separador [Vue 1];
- uma recarga quando o utilizador roda o seu dispositivo enquanto a guia [Vue 1] está a ser apresentada;
Distinguimos estes dois casos graças ao número da vista armazenado na sessão. Este número corresponde ao da última vista apresentada (linha 28).
- linha 18: ocorre navegação e não restauração se o número da última vista for diferente do da vista atual;
- linhas 21-25: incremento do contador de visitas e a sua exibição;
1.23.7. Recuperação do fragmento [PlaceHolderFragment]
A recuperação dos fragmentos é efetuada 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() {
// registo
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// de que 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 alterado
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);
// vista atual
session.setCurrentView(numView);
}
- linhas 15-16: determina-se o número da vista que está a ser atualizada;
- linhas 18-22: caso em que o fragmento se encontra num ciclo de gravação/restauração após uma alteração na orientação do dispositivo. Neste caso, é necessário restaurá-lo. Trata-se, geralmente, 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, este deve ser regenerado;
- linha 21: verifica-se que a inicialização do fragmento foi efetuada;
- linhas 24-36: tal como anteriormente para o fragmento [Vue1Fragment], o incremento do contador de visitas não deve ocorrer durante uma restauração. Tal como anteriormente, é necessário distinguir entre navegação e restauração;
- linhas 32-36: caso da 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 então a ser exibido;
- linha 38: este texto é novamente exibido;
- linha 40: regista-se na sessão o número da nova vista exibida;
1.23.8. Gestão dos separadores
Os parágrafos anteriores não abordaram a gestão dos separadores. No entanto, observámos um problema no exemplo 21 durante a rotação do dispositivo: apenas o primeiro separador, [Vue 1], foi mantido. O segundo separador, por sua vez, foi perdido.
Resolvemos este problema na classe [MainActivity] da seguinte forma:
@AfterViews
protected void afterViews() {
// registo
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);
}
// que separador selecionar?
tabLayout.getTabAt(session.getSelectedTab()).select();
...
}
- linhas 14-16: criação do primeiro separador;
- linhas 18-23: criação do segundo separador. Para saber se é necessário criá-lo, verificamos na sessão o número do fragmento exibido no separador 2. Se esse número for diferente de -1, o seu valor inicial, então o segundo separador é criado. Nesta fase, temos dois separadores, dos quais o primeiro está selecionado por predefinição;
- linha 26: procura-se na sessão o número do separador que estava selecionado antes do armazenamento/restauração e seleciona-se esse novamente. Se o campo [selectedTab] ainda não tiver sido inicializado pelo código, é utilizado o seu valor inicial 0;











































































































































































































































































































































