Skip to content

1. Aprender Programação Android

O PDF do documento está disponível |AQUI|.

Os exemplos do documento estão disponíveis |AQUI|.

1.1. Introdução

1.1.1. Índice

Este documento é uma reescrita de vários documentos existentes:

  1. Android para programadores J2EE;
  1. Introdução à Programação de Tablets Android Através de Exemplos;
  2. Controlar um Arduino com um tablet Android;
  3. Introdução à Programação de Tablets Android Através de Exemplos - Versão 2

e apresenta as seguintes novas funcionalidades:

  • O Documento 1 apresentou uma arquitetura chamada AVAT (Activity-Views-Actions-Tasks) para facilitar a programação assíncrona numa aplicação Android. Neste documento, a biblioteca padrão RxJava é utilizada para gerir ações assíncronas;
  • O Documento 2 utilizou o IDE Eclipse com um plugin Android. Este documento utiliza o Android Studio;
  • O Documento 3 é incluído tal como está;
  • O Documento 4 utilizou a biblioteca [Android Annotations] (AA) com o IDE IntelliJ IDEA Community Edition. Este documento reproduz na íntegra o Documento 4 com as seguintes diferenças:
    • o IDE é agora o Android Studio;
    • o sistema de compilação é o Gradle para todos os projetos de cliente ou servidor (no documento 4, o Maven era por vezes utilizado)
    • a programação assíncrona é implementada utilizando a biblioteca RxJava (no Documento 4, foi utilizada a biblioteca AA);
  • este documento explora áreas não abordadas, ou apenas brevemente abordadas, nos documentos anteriores:
    • o conceito de adjacência de fragmentos;
    • guardar/restaurar a atividade e os seus fragmentos;
    • o ciclo de vida do fragmento;

Por fim, apresenta o esqueleto de um cliente Android que comunica com um serviço web/JSON, no qual isolamos um grande número de elementos comumente encontrados neste tipo de cliente. Este esqueleto é utilizado em todos os exemplos a partir do Capítulo 2. Esta é a parte verdadeiramente inovadora do documento.

São apresentados os seguintes exemplos:

Exemplo
Natureza
1
Importar um projeto Android existente
2
Um projeto Android básico
3
Um projeto básico de [Anotações Android]
4
Visualizações e eventos
5
Navegação entre vistas
6
Navegação por separadores
7
Utilizar a biblioteca [Android Annotations] com o Gradle
8–12
Gerir fragmentos numa aplicação Android
13
Navegação por vistas revisitada
14
Arquitetura de duas camadas
15
Arquitetura cliente/servidor
16
Tratamento da assincronia com RxJava
17, 17B
Componentes de introdução de dados
18
Utilização de um padrão de visualização
19
O componente ListView
20
Utilização de um menu
21
Utilização de uma classe pai para fragmentos
22, 22B
Guardar e restaurar o estado da atividade e dos fragmentos
23
Cliente meteorológico
Capítulo 2
Estrutura de um cliente Android que comunica com um serviço web / JSON. Tem em conta um grande número de elementos normalmente encontrados neste tipo de cliente Android.
Capítulo 3
Gestão de consultas para um consultório médico
Capítulo 4
Exercício prático - Gestão básica de folhas de pagamento
Capítulo 5
Exercício prático - Encomenda 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 invulgar do texto. Os dois exercícios práticos são trabalhos de laboratório para os quais apenas são fornecidas as linhas gerais da solução. A solução deve ser desenvolvida pelo leitor.

O código-fonte dos exemplos está disponível |AQUI|. Para executar estes exemplos, deve seguir o procedimento descrito na secção 6.12.

Este documento é um guia introdutório à programação Android. Não pretende ser exaustivo. Destina-se principalmente a principiantes.

O site de referência para a programação Android encontra-se no URL [http://developer.android.com/guide/components/index.html]. É aí que deve ir para obter uma visão geral da programação Android.

1.1.2. Pré-requisitos

Para tirar o máximo partido deste documento, deve ter um conhecimento sólido da linguagem de programação Java.

1.1.3. Ferramentas utilizadas

Os exemplos a seguir foram testados no seguinte ambiente:

  • Máquina 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 secção 6.8);
  • o gestor do emulador Android Genymotion (ver secção 6.9);
  • o gestor de dependências Maven (ver secção 6.10);
  • o IDE [Android Studio] (ver secção 6.11);

1.2. Exemplo-01: Importar um exemplo para Android

1.2.1. Criação do projeto

Vamos criar o nosso primeiro projeto Android utilizando o Android Studio. Primeiro, vamos criar uma pasta [examples] vazia onde todos os nossos projetos serão armazenados:

  

Em seguida, crie um projeto com o Android Studio. Primeiro, vamos importar um dos exemplos incluídos no IDE [1-5]:

 

Image

A importação do projeto pode resultar em erros devido a uma incompatibilidade entre o ambiente utilizado quando o projeto foi criado e o utilizado aqui para o executar. Esta é uma oportunidade para ver como resolver este tipo de erro. 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"
}
 
// The sample build uses multiple directories to
// keep boilerplate and common code separate from
// the main sample code.
List<String> dirs = [
    'main',     // main sample code; look here for the interesting stuff.
    'common',   // components that are reused by multiple samples
    'template'] // boilerplate code that is generated by the sample template process
 
android {
    compileSdkVersion 21
    buildToolsVersion "23.0.3"
    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 21
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }
    sourceSets {
        main {
            dirs.each { dir ->
                java.srcDirs "src/${dir}/java"
                res.srcDirs "src/${dir}/res"
            }
        }
        androidTest.setRoot('tests')
        androidTest.java.srcDirs = ['tests/src']
    }
 
    aaptOptions {
        noCompress "pdf"
    }
}
  • O erro relatado deve-se às linhas 31, 34–35: não temos o SDK 21. Substituímos esta versão pela versão 23, que temos.

No ficheiro [build.gradle], o Android Studio apresenta sugestões, conforme mostrado abaixo:

 

Para aceitar as sugestões, prima [Alt-Enter] na sugestão:

 

Também poderá ocorrer um erro relacionado com a versão do Gradle:

 

Este erro decorre de uma incompatibilidade entre a versão do Gradle exigida pelo ficheiro [build.gradle] do projeto (versão 2.10 na linha 6 abaixo):


buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}

e o que está listado no ficheiro [<project>/gradle/wrapper/gradle-wrapper.properties]:


#Wed Apr 10 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, substitua 2.8 por 2.10.

Para aceder ao ficheiro [<project>/gradle/wrapper/gradle-wrapper.properties], utilize a vista do projeto:

Depois de corrigir isto, pode compilar a aplicação [1], iniciar o emulador Genymotion [2] e executar o projeto [3]:

 

Image

Vamos parar a aplicação:

  

Já pode fechar o projeto. Vamos criar um novo.

  

1.2.2. Algumas notas sobre o IDE

1.2.2.1. Vistas

O IDE Android Studio (AS) oferece diferentes vistas para trabalhar com um projeto. Iremos utilizar principalmente duas:

  • a vista [Android] [1]:
  • a vista [Project] [4];
 
  

Na maioria das vezes, trabalharemos com a vista [Android]. Quando clonarmos um projeto para outro, precisaremos da vista [Projeto].

1.2.2.2. Gestão de Execução

Existem várias formas de executar, parar ou reexecutar um projeto AS. Em primeiro lugar, existem os botões na barra de ferramentas:

O botão [Rerun] [3] interrompe o 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 para tornar o IDE o mais responsivo possível. Com a versão 2.1 do Android Studio (maio de 2016), este cache muitas vezes não refletia as alterações de código que acabavam de ser feitas. Neste caso, deve invalidar o cache:

Com o Android 2.1 (maio de 2016), o passo anterior tinha de ser executado várias vezes e, por vezes, isso não era suficiente para resolver o problema detetado. A solução consistia em desativar o [Instant Run]:

  • em [3-4], tudo foi desativado;

Em todos os casos a seguir, trabalhámos com esta configuração de cache e não encontrámos quaisquer problemas.

1.2.2.4. Gestão de registos

Ao executar um projeto, os registos são apresentados no Android Monitor:

No separador [Android Monitor] [1], os registos são apresentados no separador [logcat] [2]. O botão [3] permite limpar os registos. Este botão é útil quando pretende visualizar os registos de uma ação específica:

  • limpar os registos;
  • no dispositivo Android, execute a ação para a qual pretende os registos;
  • os registos que aparecem são os relacionados com a ação realizada;

Existem vários níveis de registo [4]. Por predefinição, o modo [Verbose] está selecionado. Isto significa que são apresentados registos de todos os níveis. Pode utilizar [4] para selecionar um nível específico.

Os registos são muito úteis para determinar em que pontos, durante a execução de um projeto, determinados métodos são executados. Iremos utilizá-los frequentemente. Vejamos o código da classe [MainActivity] no projeto [Example-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, linha 14] e [onCreateOptionsMenu, linha 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, pode ser difícil determinar se um determinado método do ciclo de vida será executado antes ou depois de um método que nós próprios escrevemos. No entanto, esta informação é frequentemente importante de se saber. Podemos, portanto, adicionar registos como mostrado abaixo:


public class MainActivity extends Activity {
 
  public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity","onCreate");
    super.onCreate(savedInstanceState);
    ...
  }
 
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    Log.d("MainActivity","onCreateOptionsMenu");
    getMenuInflater().inflate(R.menu.main, menu);
   ...
  }
 
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("MainActivity","onOptionsItemSelected");
    switch (item.getItemId()) {
      ...
  }
}
  • As linhas 7, 14 e 21 utilizam a classe [Log]. Esta classe permite-lhe escrever registos na consola do Android [logcat]. Os registos são classificados em vários níveis (info, warning, debug, verbose, error). [Log.d] exibe registos de nível [debug]. O seu primeiro argumento é a fonte da mensagem de registo. De facto, várias fontes podem enviar mensagens para a consola de registos. Para distinguir entre elas, utilizamos este primeiro argumento. O segundo argumento é a mensagem a ser gravada na consola de registos;

Se executarmos novamente o projeto [Exemplo-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

Podemos ver que o método [onCreate], que cria a atividade do 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 frequentemente adicionar instruções de registo ao código Android. Na maioria das vezes, não iremos comentá-las. Estão lá simplesmente para incentivar o leitor a consultar a consola de registo, de modo a compreender gradualmente o ciclo de vida de uma aplicação Android.

1.2.2.5. Gerir o Emulador [Genymotion]

Por vezes, o emulador Genymotion bloqueia e não pode ser reiniciado. Isto acontece porque os processos do VirtualBox continuam a ser executados no Gestor de Tarefas. Abra o Gestor de Tarefas [Ctrl-Alt-Del] e elimine todos os processos do VirtualBox:

Depois de fazer isso, reinicie o emulador Genymotion a partir do Android Studio.

1.2.2.6. Gerir o ficheiro binário APK criado

A compilação do projeto produz um ficheiro binário com a extensão .apk:

Existem duas versões: uma chamada [debug] e outra chamada [debug-unaligned]. Deve utilizar a primeira; a outra é uma versão intermédia. O ficheiro binário .apk gerado em [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 um novo projeto Android utilizando o Android Studio [1-12]:

 

Em [13], executamos a aplicação. Em seguida, vemos o ecrã apresentado em [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 utilizando as suas definições de configuração. Trata-se de um ficheiro mínimo que iremos expandir gradualmente.

  • linhas 3–12: as características da aplicação Android;
  • linhas 22–25: as suas dependências. É aqui que faremos principalmente alterações com base nos 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 das entradas feitas quando o projeto foi criado:

  • A linha 3 do manifesto (pacote) provém da entrada [4] acima. Várias classes são geradas automaticamente 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 da aplicação [2]. Pode ser alterado;
  • linha 7: o rótulo 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. Linha 2: o nome da aplicação provém da entrada efetuada durante a compilaçã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 baseia-se 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 [AppCompatActivity] do Android. Este será o caso para todas as atividades futuras;
  • linha 9: O método [onCreate] é executado quando a atividade é criada. Isto acontece antes de a vista associada à atividade ser apresentada;
  • linha 10: o método [onCreate] da classe pai é chamado. Isto deve ser sempre feito;
  • 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 layout. A escolha padrão é o tipo [RelativeLayout]. Neste tipo de contentor, os componentes são posicionados uns em relação aos outros (à direita, à esquerda, abaixo, acima);
  • linhas m-p: um componente [TextView] utilizado para apresentar texto;
  • linha n: o texto exibido. Não é recomendável codificar texto diretamente nas vistas. É preferível mover este texto para o ficheiro [res/values/strings.xml] [3]:

O texto exibido será, portanto, [Hello World!]. Onde será exibido? O contentor [RelativeLayout] preencherá o ecrã. O [TextView], que é o seu único elemento, será exibido na parte superior e à esquerda deste contentor e, portanto, na parte superior e à esquerda do ecrã;

O que significa [R.layout.activity_main] na linha 11? A cada recurso Android (visualizações, fragmentos, componentes, etc.) é atribuído um identificador. Assim, uma visualização [V.xml] localizada na pasta [res/layout] será identificada como [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] corresponde ao ID da string [app_name] no ficheiro [res/values/string.xml]:
  • Linha 19: O atributo [R.mipmap.ic_launcher] é o identificador da imagem [res/mipmap/ic_launcher];

Portanto, lembre-se de que, quando faz referência a [R.layout.activity_main] no código, está a fazer referência a um atributo da classe [R]. O IDE ajuda-o a identificar os diferentes elementos desta classe:

1.3.4. Executar a aplicação

Para executar uma aplicação Android, precisamos de criar uma configuração de execução:

  • Em [1], selecione [Editar configurações];
  • O projeto foi criado com uma configuração [app], que iremos eliminar [2] para o recriar;
  • em [3], crie uma nova configuração de execução;
  
  • em [4], selecione [Aplicação Android];

Image

  • em [5], selecione o módulo [app] na lista suspensa;
  • Em [6-8], mantenha os valores predefinidos;
  • Em [7], a atividade predefinida é 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 [Mostrar caixa de diálogo de seleção] para escolher o dispositivo no qual a aplicação será executada (emulador, tablet);
  • Em [9], especifique que esta escolha deve ser guardada;
  • Confirme a configuração;
  
  • Em [11], inicie o gestor de emuladores [Genymotion] (ver secção 6.9);
  • Em [12], selecione um emulador de tablet e inicie-o [13];
  • em [14], execute a configuração de execução do [app];
  • em [15], é apresentado o formulário de seleção do dispositivo de execução. Aqui, está disponível apenas uma opção: o emulador [Genymotion] iniciado anteriormente;

Após alguns instantes, o emulador de software apresenta a seguinte visualização:

Image

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 é um dos métodos que podem ser chamados durante o ciclo de vida de uma atividade. A documentação do Android enumera estes métodos:

 
  • [1]: O método [onCreate] é chamado quando a atividade é iniciada. É neste método que a atividade é associada a uma vista e as referências aos seus componentes são recuperadas;
  • [2-3]: Os métodos [onStart] e [onResume] são então chamados. Note-se que o método [onResume] é o último método a ser executado antes de a atividade atualmente em execução atingir o estado [4];

1.4. Exemplo-03: Reescrever o projeto [Exemplo-02] utilizando a biblioteca [Android Annotations]

Vamos agora apresentar a biblioteca [Android Annotations], que facilita a escrita de aplicações Android. Para tal, duplique o exemplo [Exemplo-02] para [Exemplo-03] seguindo os passos [1-16].

  • Em [1], selecione a vista [Project] para ver todo o projeto Android;

Nota: Entre [14] e [15], mudámos da vista [Android] para a vista [Projeto] (ver secção 1.2.2.1).

Em seguida, modificamos o ficheiro [res/values/strings.xml] [17]:

 

O ficheiro [strings.xml] é modificado da seguinte forma:


<resources>
  <string name="app_name">Exemple-03</string>
</resources>

Agora, executamos a nova aplicação, que manteve toda a configuração do [Exemplo-02]:

 

Em [19], obtemos o mesmo resultado que em [Exemplo-02], mas com um novo nome.

Vamos agora apresentar a biblioteca [Android Annotations], que designaremos por AA, para abreviar. Esta biblioteca introduz novas classes para anotar o código-fonte Android. Estas anotações serão utilizadas por um processador que criará novas classes Java no módulo; estas classes participarão na compilação do módulo tal como as classes escritas pelo programador. Temos, assim, a seguinte cadeia de compilação:

Primeiro, vamos adicionar as dependências para o compilador de anotações AA (o processador mencionado acima) ao ficheiro [build.gradle]:


def AAVersion = '4.0.0'
 
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile fileTree(dir: 'libs', include: ['*.jar'])
}
  • As linhas 4–5 adicionam as duas dependências que compõem a biblioteca AA;

O ficheiro [build.gradle] é novamente modificado para utilizar um plugin chamado [android-apt], que divide o processo de compilação em duas etapas:

  • processamento das anotações Android, que gera novas classes;
  • compilação de todas as classes do projeto;

buildscript {
  repositories {
    mavenCentral()
  }
 
  dependencies {
    // Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}
 
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
  • 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 altura, verifique se a configuração de execução [app] continua a funcionar.

Vamos agora introduzir a primeira anotação da biblioteca AA na classe [MainActivity]:

  

A classe [MainActivity] tem atualmente o seguinte aspeto:


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 na secção 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á gerar uma classe [MainActivity_] derivada da classe [MainActivity], e esta classe será a atividade propriamente dita. Devemos, portanto, modificar o manifesto do projeto [AndroidManifest.xml] da seguinte forma:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exemples.android">
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity_">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
 
</manifest>
  • linha 11: a nova atividade;

Depois de fazer isto, podemos compilar o projeto [1]:

 
  • Em [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.
// Generated using AndroidAnnotations 4.0.0.
// 
// You can create a larger work that contains this file and distribute that work under terms of your choice.
//
 
 
package exemples.android;
 
import android.app.Activity;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import org.androidannotations.api.builder.ActivityIntentBuilder;
import org.androidannotations.api.builder.PostActivityStarter;
import org.androidannotations.api.view.HasViews;
import org.androidannotations.api.view.OnViewChangedNotifier;
 
public final class MainActivity_
    extends MainActivity
    implements HasViews
{
    private final OnViewChangedNotifier onViewChangedNotifier_ = new OnViewChangedNotifier();
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        OnViewChangedNotifier previousNotifier = OnViewChangedNotifier.replaceNotifier(onViewChangedNotifier_);
        init_(savedInstanceState);
        super.onCreate(savedInstanceState);
        OnViewChangedNotifier.replaceNotifier(previousNotifier);
        setContentView(R.layout.activity_main);
    }
...
  • linhas 24-25: a classe [MainActivity_] estende a classe [MainActivity];

Não tentaremos explicar o código das classes geradas pelo AA. Estas lidam com a complexidade que as anotações pretendem ocultar. Mas, por vezes, pode ser útil examiná-lo quando se pretende compreender como as anotações utilizadas são «traduzidas».

Podemos agora executar novamente a configuração [app]. Obtemos o mesmo resultado que antes. Vamos agora usar este projeto como ponto de partida e duplicá-lo para introduzir os conceitos-chave da programação Android.

1.5. Exemplo-04: Visualizações e Eventos

1.5.1. Criação do projeto

Seguiremos o procedimento descrito para duplicar o [Exemplo-02] no [Exemplo-03] na secção 1.4:

Nós:

  • duplicamos o projeto [Exemplo-03] para [Exemplo-04] (após eliminar a pasta [app/build] do [Exemplo-03]);
  • carregamos o projeto [Exemplo-04];
  • alteramos o nome do projeto no ficheiro [app / res / values / strings.xml] (perspetiva Android);
  • Apagamos o ficheiro [Exemplo-04 / Exemplo-04.iml] (vista do projeto);
  • compilamos e, em seguida, executamos o projeto;
 

1.5.2. Criação de uma vista

Vamos agora utilizar o editor gráfico para modificar a vista apresentada pelo projeto [Exemplo-04]:

  • Em [1-4], crie uma nova vista XML;
  • Em [5], nomeie a vista;
  • Em [6], especifique a tag raiz da vista. Aqui, escolhemos um contentor [RelativeLayout]. Dentro deste contentor de componentes, os componentes são posicionados uns em relação aos outros: «à direita de», «à esquerda de», «abaixo», «acima»;
  

O ficheiro [vue1.xml] gerado [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], mude para o modo tablet;
  • em [5], defina a escala para 1 para o tablet;
  • Em [6], selecione o modo «paisagem» para o tablet;
  • A captura de ecrã [7] resume as opções selecionadas.
  • Em [1], selecione um [Texto Grande] e arraste-o para a vista [2];
  • Em [3], clique duas vezes no componente;
  • Em [4], edite o texto apresentado. Em vez de o codificar diretamente na vista XML, iremos externalizá-lo no ficheiro [res/values/string.xml]
  • Em [5], adicione um novo valor ao ficheiro [strings.xml];
  • em [8], atribua um identificador à string;
  • em [9], atribua o valor da string;
  • em [10], a nova visualização após a validação do passo anterior;
  • depois de clicar duas vezes no componente, alteramos o seu ID [11];
  • em [12], nas propriedades do componente, altere o tamanho da fonte [50pt];
  • em [13], a nova visualização;

O ficheiro [vue1.xml] foi alterado da seguinte forma:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
 
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="@string/titre_vue1"
    android:id="@+id/textViewTitreVue1"
    android:layout_marginLeft="213dp" android:layout_marginStart="213dp"
    android:layout_marginTop="50dp" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true" android:textSize="50sp"/>
</RelativeLayout>
  • As alterações feitas na GUI encontram-se nas linhas 10, 11 e 14. Os outros atributos do [TextView] são valores predefinidos ou resultam do posicionamento do componente dentro da vista;
  • linhas 7–8: o tamanho do componente corresponde ao do texto que contém (wrap_content), tanto em altura como em 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 o lado esquerdo da vista (linha 13), 213 píxeis à direita (linha 12);

Geralmente, 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:

N.º
Id
Tipo
Função
1
textViewTitleView1
TextView
Título da vista
2
textView1
TextView
uma pergunta
3
editarNome
EditText
introduza um nome
4
botãoValidar
Botão
para confirmar a entrada
5
botãoView2
Botão
para mudar para a vista n.º 2

Posicionar componentes uns em relação aos outros pode ser frustrante, uma vez que o comportamento do editor gráfico é, por vezes, imprevisível. Pode ser melhor utilizar as propriedades dos componentes:

O componente [textView1] deve ser colocado 50 píxeis abaixo do título e a 50 píxeis da borda esquerda do contentor:

  • em [1], a borda superior do componente está alinhada com a borda inferior do componente [textViewTitreVue1] a uma distância de 50 píxeis [3] (superior);
  • em [2], a borda esquerda (esquerda) do componente está alinhada com a borda esquerda do contêiner a uma distância de 50 pixels [3] (esquerda);

O componente [editTextNom] deve ser colocado 60 píxeis à direita do componente [textView1] e alinhado na parte inferior com esse mesmo componente;

 
  • em [1], a borda esquerda do componente está alinhada com a borda direita do componente [textView1] a uma distância de 60 píxeis [2] (esquerda). 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 na parte inferior com esse mesmo componente;

 
  • Em [1], a borda esquerda do componente está alinhada com a borda direita do componente [editTextNom] a uma distância de 60 píxeis [2] (esquerda). Está alinhada com a borda inferior do componente [editTextNom] (bottom:bottom) [1];

O componente [buttonVue2] deve ser posicionado 50 píxeis abaixo do componente [textView1] e alinhado à esquerda desse componente;

 
  • em [1], a borda esquerda do componente está alinhada com a borda esquerda do componente [textView1] e está posicionada abaixo deste (top:bottom) a uma distância de 50 píxeis [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>

Isto contém todos os elementos gráficos. Outra forma de criar uma vista é editar este ficheiro diretamente. Depois de se habituar, isto pode ser mais rápido do que utilizar o editor gráfico.

  • Na linha 38, há informações que não mostrámos. São fornecidas através das propriedades do componente [editTextNom] [1]:
 

Todo o texto 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 [MainActivity] para que esta vista seja apresentada quando a aplicação for iniciada:


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] é agora apresentada pela atividade;

Modifique 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] é exibida. Isto acontece porque a vista tem um campo de entrada que recebe o foco quando a vista é exibida. Por predefinição, este foco faz com que o teclado virtual apareça;

Execute a aplicação e verifique se a vista [view1.xml] é efetivamente apresentada:

Image

1.5.3. Tratamento de eventos

Agora, vamos tratar do clique no botão [Validate] na vista [View1]:

Image

O código para [MainActivity] altera-se da seguinte forma:


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 {
 
  // visual interface elements
  @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");
  }
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(this, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
}
  • Linhas 17–18: Associamos o campo [protected EditText editTextNom] ao componente identificado por [R.id.editTextNom] na interface visual. O campo associado ao componente deve ser acessível na classe derivada [MainActivity_] e, por esse motivo, não pode ter um escopo [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)] especifica o método que trata do evento «Click» no botão com o ID [R.id.buttonValider]. Este ID 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: exibe o nome introduzido:
    • Toast.makeText(...).show(): exibe texto no ecrã,
    • o primeiro parâmetro de makeText é a atividade,
    • o segundo parâmetro é o texto a exibir na caixa de diálogo que será apresentada pelo makeText,
    • o terceiro parâmetro é a duração da caixa exibida: Toast.LENGTH_LONG ou Toast.LENGTH_SHORT;
  • na linha 26, a anotação [@AfterViews] indica que o método deve ser executado assim que todos os campos anotados com [@ViewById] tiverem sido inicializados. É importante saber quando estes campos são inicializados. Por exemplo, podemos utilizar a referência da linha 18 no método [onCreate]? Para responder a esta questão, adicionámos registos;

Execute o projeto [Example-04] e verifique se algo acontece quando clica no botão [Validate]. Obtemos os seguintes registos:

05-28 09:06:23.751 571-571/exemples.android D/MainActivity: onCreate
05-28 09:06:23.841 571-571/exemples.android D/MainActivity: afterViews

Concluímos que, quando o método [onCreate] é executado, os campos anotados com [@ViewById] ainda não estão inicializados. Mais uma vez, encorajamos os leitores iniciantes a incluírem este tipo de registo nos métodos que gerem o ciclo de vida da aplicação.

1.6. Exemplo-05: Navegar entre vistas

No projeto anterior, o botão [View 2] não foi utilizado. Propomos utilizá-lo criando uma segunda vista e demonstrando como navegar entre vistas. Existem várias formas de resolver este problema. A abordagem aqui proposta consiste em associar cada vista a uma atividade. Outro método consiste em ter uma única [AppCompatActivity] que exibe vistas [Fragment]. Este será o método utilizado em futuras aplicações.

1.6.1. Criação do projeto

Duplicamos o projeto [Exemplo-04] para [Exemplo-05]. Para tal, seguiremos o procedimento descrito para duplicar [Exemplo-02] para [Exemplo-03] na Secção 1.4, que foi reproduzido na Secção 1.5.

1.6.2. Adicionar uma segunda atividade

Para gerir uma segunda vista, vamos criar uma segunda atividade. Esta atividade irá gerir a vista n.º 2. Estamos a seguir aqui um modelo de uma vista por atividade. São possíveis outros modelos.

Image

  • Em [1-4], criamos uma nova atividade;

Image

  • em [5], o nome da classe que será gerada;
  • em [6], o nome da vista (view2.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>

Atualmente, esta é uma vista vazia com um gestor de layout [RelativeLayout] (linha 2). Na linha 11, podemos ver que foi associada à nova atividade.

O manifesto do módulo Android [AndroidManifest.xml] foi alterado 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>
    <activity android:name=".SecondActivity">
    </activity>
  </application>
 
</manifest>

Linha 20: foi registada uma segunda atividade.

1.6.3. Navegar da Vista 1 para a Vista 2

Voltemos ao código da classe [MainActivity], que apresenta a Vista 1. A transição para a Vista 2 ainda não está implementada:

  

Procedemos da seguinte forma:


  // navigate to view no. 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
    // navigate to view no. 2 by passing it the name entered in view no. 1
    // create an Intent
    Intent intent = new Intent();
    // we associate this Intent with an activity
    intent.setClass(this, SecondActivity.class);
    // we associate information with this Intent
    intent.putExtra("NOM", editTextNom.getText().toString().trim());
    // launch the [SecondActivity] activity by passing it the Intent
    startActivity(intent);
}
  • linhas 2-3: o método [navigateToView2] trata do «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 alteração da vista:

  1. linha 6: crie um objeto do tipo [Intent]. Este objeto especificará tanto a atividade a ser iniciada como as informações a serem passadas para ela;
  2. linha 8: associe o Intent a uma atividade, neste caso uma atividade do tipo [SecondActivity] que será responsável por exibir a vista n.º 2. Lembre-se de que a [MainActivity] exibe a vista n.º 1. Portanto, temos uma vista = uma atividade. Teremos de definir o tipo [SecondActivity];
  3. Linha 10: Opcionalmente, adicione informações ao objeto [Intent]. Estas informações destinam-se à [SecondActivity] que será iniciada. Os parâmetros para [Intent.putExtra] são (Object key, Object value). Note que o método [EditText.getText()], que devolve o texto introduzido no campo de texto, não devolve um tipo [String], mas sim um tipo [Editable]. Deve utilizar o método [toString] para obter o texto introduzido;
  4. Linha 12: Inicie a atividade definida pelo objeto [Intent].

Execute o projeto [Example-05] e verifique se vê a Vista #2 (vazia por enquanto):

1.6.4. A criar a Vista #2

 
  • Em [1-2], removemos a vista [main.xml], da qual já não precisamos, e, em seguida, modificamos a vista [vue2.xml] da seguinte forma:
 

Os componentes são os seguintes:

N.º
Id
Tipo
Função
1
textViewTitleView2
TextView
Título da vista
2
textViewOlá
TextView
algum texto
5
btn_view1
Botão
para ir para a visualização n.º 1

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 [Example-05] e verifique se a nova vista é apresentada quando clica no botão [View #2].

1.6.5. A atividade [SecondActivity]

Em [MainActivity], escrevemos o seguinte código:


    // navigate to view no. 2
    protected void navigateToView2() {
        // navigate to view no. 2 by passing it the name entered in view no. 1
        // create an Intent
        Intent intent = new Intent();
        // we associate this Intent with an activity
        intent.setClass(this, SecondActivity.class);
        // we associate information with this Intent
        intent.putExtra("NOM", edtNom.getText().toString().trim());
        // launch the [SecondActivity] activity by passing it the Intent
        startActivity(intent);
}

Na linha 9, passámos informações para a [SecondActivity] que não foram utilizadas. Agora estamos a utilizá-las, e isto acontece no código da [SecondActivity]:

  

O código da [SecondActivity] altera-se 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 {
 
  // visual interface components
  @ViewById
  protected TextView textViewBonjour;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
 
  @AfterViews
  protected void afterViews() {
    // recover intent if it exists
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // we retrieve the name
        String nom = extras.getString("NOM");
        if (nom != null) {
          // we display it
          textViewBonjour.setText(String.format("Bonjour %s !", nom));
        }
      }
    }
  }
 
}
  • linha 11: usamos a anotação [@EActivity] para indicar que a classe [SecondActivity] é uma atividade associada à vista [vue2.xml];
  • linhas 15–16: recuperamos uma referência ao componente [TextView] identificado por [R.id.textViewBonjour]. Aqui, não escrevemos [@ViewById(R.id.textViewBonjour)]. Neste caso, o AA assume que o identificador do componente é idêntico ao campo anotado, neste caso o campo [textViewBonjour];
  • linha 23: a anotação [@AfterViews] marca um método que deve ser executado após os campos anotados com [@ViewById] terem sido inicializados. No método [OnCreate] (linha 19), estes campos não podem ser utilizados porque ainda não foram inicializados. No projeto [Example-05], alternamos de uma atividade para outra, e inicialmente não estava claro se o método anotado com [@AfterViews] seria executado uma vez durante a instanciação inicial da atividade ou sempre que a atividade fosse iniciada. Os testes mostraram que a segunda hipótese estava correta;
  • linha 26: a classe [AppCompatActivity] possui um método [getIntent] que retorna o objeto [Intent] associado à atividade;
  • linha 28: o método [Intent.getExtras] devolve um objeto [Bundle], que é uma espécie de dicionário contendo informações associadas ao objeto [Intent] da atividade;
  • linha 31: recuperamos o nome armazenado no objeto [Intent] da atividade;
  • linha 34: exibimo-lo.

Lembrete: os campos anotados 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 {

O AA irá gerar uma classe [SecondActivity_] derivada de [SecondActivity], e esta classe será a atividade real. Isto leva-nos a fazer alterações em:

[MainActivity]


  // navigate to view no. 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
..
    // we associate this Intent with an activity
    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, substitua [SecondActivity] por [SecondActivity_];

Teste esta nova versão. Digite um nome na vista n.º 1 e verifique se a vista n.º 2 o exibe corretamente.

1.6.6. Navegar da Vista #2 para a Vista #1

Para navegar da Vista #2 para a Vista #1, seguiremos o procedimento visto anteriormente:

  • Coloque o código de navegação na atividade [SecondActivity] que exibe a Vista 2;
  • escreva o método [@AfterViews] na [MainActivity] que exibe a Vista 1;

O código para [SecondActivity] altera-se da seguinte forma:


  @Click(R.id.buttonVue1)
  protected void navigateToView1() {
    // we create an Intent for activity [MainActivity]
    Intent intent1 = new Intent();
    intent1.setClass(this, MainActivity_.class);
    // retrieve the Intent of the current activity [SecondActivity]
    Intent intent2 = getIntent();
    if (intent2 != null) {
      Bundle extras2 = intent2.getExtras();
      if (extras2 != null) {
        // we put the name in the Intent of [MainActivity]
        intent1.putExtra("NOM", extras2.getString("NOM"));
      }
      // launch [MainActivity]
      startActivity(intent1);
    }
}
  • linhas 1-2: associamos o método [navigateToView1] a um clique no botão [btn_vue1];
  • linha 4: criamos um novo [Intent];
  • linha 5: associado à atividade [MainActivity_];
  • linha 7: recuperamos o Intent associado a [SecondActivity];
  • linha 9: recuperamos as informações deste Intent;
  • linha 12: a chave [NAME] é recuperada de [intent2] e colocada em [intent1] com o mesmo valor associado;
  • linha 15: a atividade [MainActivity_] é iniciada.

No código da [MainActivity], adicionamos o seguinte método [@AfterViews]:


  @AfterViews
  protected void afterViews() {
    // recover intent if it exists
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // we retrieve the name
        String nom = extras.getString("NOM");
        if (nom != null) {
          // we display it
          editTextNom.setText(nom);
        }
      }
    }
}

Faça estas alterações e teste a sua aplicação. Agora, quando voltar da Vista 2 para a Vista 1, o nome que introduziu originalmente deverá ser apresentado, o que não acontecia até agora.

1.6.7. Ciclo de vida da atividade

Na Secção 1.3.5, apresentámos o ciclo de vida de uma atividade. Aqui temos duas atividades e alternamos entre elas durante a execução. Estas atividades contêm dois métodos — [onCreate] e [afterViews] — e não é imediatamente claro quando um é chamado em relação ao outro. É importante saber isto. Para descobrir, vamos adicionar registos a ambas as atividades:

Assim, na classe [MainActivity], escrevemos:


  // manufacturer
  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 várias vezes;
  • linha 8: queremos saber se o método [onCreate] é chamado uma ou várias vezes;
  • linha 14: queremos saber se o método [afterViews] é chamado uma ou várias vezes;

Fazemos exatamente o mesmo na classe [SecondActivity].

Quando a aplicação é iniciada, vemos os seguintes registos:

1
2
3
05-28 09:38:09.429 26711-26711/exemples.android D/MainActivity: constructor
05-28 09:38:09.449 26711-26711/exemples.android D/MainActivity: onCreate
05-28 09:38:09.600 26711-26711/exemples.android D/MainActivity: afterViews

Os métodos [onCreate, afterViews] da primeira atividade foram executados nesta ordem. Quando clica no botão [View #2], os novos registos são os seguintes:

1
2
3
05-28 09:39:26.607 26711-26711/exemples.android D/SecondActivity: constructor
05-28 09:39:26.608 26711-26711/exemples.android D/SecondActivity: onCreate
05-28 09:39:26.617 26711-26711/exemples.android D/SecondActivity: afterViews

Os métodos [onCreate, afterViews] da segunda atividade foram executados nesta ordem. Quando clica no botão [View #1], os novos registos são os seguintes:

1
2
3
05-28 09:39:56.393 26711-26711/exemples.android D/MainActivity: constructor
05-28 09:39:56.394 26711-26711/exemples.android D/MainActivity: onCreate
05-28 09:39:56.400 26711-26711/exemples.android D/MainActivity: afterViews

A classe [MainActivity] é, portanto, instanciada novamente. Quando clica no botão [View #2], os novos registos são os seguintes:

1
2
3
05-28 09:40:59.099 26711-26711/exemples.android D/SecondActivity: constructor
05-28 09:40:59.102 26711-26711/exemples.android D/SecondActivity: onCreate
05-28 09:40:59.113 26711-26711/exemples.android D/SecondActivity: afterViews

A classe [SecondActivity] é, portanto, instanciada novamente.

Ambas as atividades são, portanto, recriadas sistematicamente sempre que a atividade é alterada.

Vamos agora explorar uma arquitetura com uma única atividade capaz de gerir múltiplas vistas, chamadas 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

Aqui, exploraremos interfaces com separadores. O exemplo é complexo, mas apresenta todos os elementos que utilizaremos mais tarde: atividade única, gestor de fragmentos (vistas), contentor de fragmentos, navegação entre fragmentos. O conceito de separadores difere do de fragmentos e é secundário em relação ao que pretendemos demonstrar neste exemplo.

1.7.1. Criação do projeto

Criamos um novo projeto:

 
  • em [7], selecione uma atividade com separadores (Tabbed Activity);
  • em [10-14], mantenha os valores predefinidos;
  • em [15], selecione separadores com uma barra de título;

O projeto resultante é o seguinte:

 
  • em [1], a atividade;
  • em [2], as perspetivas;

Foi criada automaticamente uma configuração de tempo de execução [app], com o nome do módulo [2b]:

 

Pode executá-la. Aparece então uma janela com três separadores [3-6]:

Image

1.7.2. Configuração do Gradle

O projeto [Exemplo-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á um novo elemento em comparação com o que vimos anteriormente: 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 à [MainActivity] do projeto. No modo [design], a vista tem o seguinte aspeto:

Image

Contém os seguintes componentes:

  
  • [main_content] é a visualização completa;
  • [appbar] (caixa vermelha, 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ítulos das guias;
  • [container] (caixa verde, 2) pode conter vários fragmentos. Um fragmento é uma vista. Assim, a mesma atividade pode exibir várias vistas (fragmentos) neste contentor;
  • [fab] (componente 3) é chamado de componente flutuante;

No modo [texto], 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>

Vemos os elementos descritos anteriormente:

  • linhas 2–49: a definição do componente [main_content] (linha 5), que constitui toda a vista. Podemos ver que se trata de um layout [CoordinatorLayout] (linha 2);
  • linhas 11–33: o contentor [appbar] (linha 12). Trata-se de um [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). Irá apresentar os títulos dos separadores;
  • linhas 35–39: o componente [container] (linha 36). Este contentor exibe as diferentes vistas da atividade;
  • linhas 41–47: o componente [fab] (linha 42) do tipo [FloatingActionButton] (linha 41). Trata-se de um botão que pode ser clicado. Por predefinição, está posicionado no canto inferior direito de toda a vista;

Não vamos tentar compreender o significado de todos os atributos destes componentes. Vamos utilizá-los tal como estão. É através da experiência — e frequentemente no modo [design] — que descobrimos as suas funções. Neste modo, verificamos que os componentes têm dezenas de atributos. Geralmente, apenas alguns são inicializados, enquanto os outros mantêm os seus valores predefinidos.

Vamos esclarecer alguns pontos, no entanto. 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] refere-se ao ficheiro [res/values/dimens.xml]:


<resources>
  <!-- Default screen margins, per the Android Design guidelines. -->
  <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] refere-se à linha (f) acima;

Da mesma forma, 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 corresponde à complexidade da vista descrita acima: é complexo. Vamos analisá-lo em várias etapas.

1.7.4.1. Gestão de fragmentos e separadores

O código em [MainActivity] relacionado com 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 {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container 
  private ViewPager mViewPager;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      // parent
    super.onCreate(savedInstanceState);
    // view
    setContentView(R.layout.activity_main);
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // the fragment manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
 
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i delivered by the fragment manager
    mViewPager = (ViewPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // the tab bar is also associated with the fragment container
    // i.e. tab n° i displays fragment n° i of the container
    TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
    tabLayout.setupWithViewPager(mViewPager);
   }
 
 
  // a fragment
  public static class PlaceholderFragment extends Fragment {
 ...
  }
 
  // the fragment manager
  // it is used to request fragments to be displayed in the main view
  // must define methods [getItem] and [getCount] - the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
  }
}
  • linha 28: O Android fornece um contentor de visualização do tipo [android.support.v4.view.ViewPager] (linha 12). Este contentor deve ser fornecido com um gestor de visualizações ou fragmentos. O programador é responsável por o fornecer;
  • 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 quando a atividade é criada;
  • linha 35: a vista [activity_main.xml] está associada à atividade;
  • linha 37: recuperamos a referência ao componente [toolbar] da vista através do seu identificador;
  • linha 38: esta barra de ferramentas torna-se a barra de ação da atividade (um conceito do Android);
  • linha 40: o gestor de fragmentos é instanciado. O parâmetro do construtor é a classe Android [android.support.v4.app.FragmentManager] (linha 10);
  • linha 44: recuperamos a referência ao contentor de fragmentos da vista [activity_main.xml] através do seu ID;
  • linha 45: o gestor de fragmentos é ligado ao contentor de fragmentos. Isto significa que, quando for solicitado ao contentor de fragmentos que exiba o fragmento #i, este irá solicitá-lo ao gestor de fragmentos;
  • linha 48: recuperamos 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 o separador #i é clicado, o contentor exibirá o fragmento #i. A associação entre o gestor de separadores e o contentor de fragmentos elimina a necessidade de gestão de separadores. Assim, não precisamos de definir um manipulador de eventos para clicar num separador. A associação com o contentor de fragmentos fornece isto por predefinição. Veremos um exemplo em que há mais fragmentos do que separadores. Neste caso, não fazemos esta associação.

O manipulador de fragmentos [SectionsPagerAdapter] é o seguinte:


// the fragment manager
  // it is used to request fragments to be displayed in the main view
  // must define methods [getItem] and [getCount] - the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }
 
    // fragment n° position
    @Override
    public Fragment getItem(int position) {
      // instantiate a fragment [PlaceHolder] and render it
      return PlaceholderFragment.newInstance(position + 1);
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      return 3;
    }
 
    // optional - gives a title to managed fragments
    @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 da própria aplicação. 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 fornecido. 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 devolve 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 nas linhas 27–33 serão os títulos dos separadores;

  • Linhas 18–21: getCount devolve o número de fragmentos geridos, neste caso três;
  • linhas 11–15: getItem(i) devolve o fragmento #i. Aqui, todos os fragmentos serão do mesmo tipo, [PlaceholderFragment];
  • linhas 24–35: getPageTitle(int i) devolve o título do fragmento #i;

1.7.4.2. Os fragmentos exibidos

  

Os fragmentos da atividade são todos do mesmo tipo aqui 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 [RelativeLayout];
  • linhas 11-14: o único componente da vista (fragmento): um [TextView] identificado por [section_label];

Em [MainActivity], os fragmentos geridos são do seguinte tipo [PlaceholderFragment]:


// a fragment
  public static class PlaceholderFragment extends Fragment {
      // a text displayed in the fragment
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    public PlaceholderFragment() {
    }
 
    // renders a fragment with one piece of information: the fragment number passed as a parameter
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      // on-board info
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // result
      return fragment;
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // view [fragment_main] is instantiated
      View rootView = inflater.inflate(R.layout.fragment_main, container, false);
      // the [TextView] is found
      TextView textView = (TextView) rootView.findViewById(R.id.section_label);
      // its content is modified
      textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
      // we return the view
      return rootView;
    }
  }
  • linha 2: a classe [PlaceholderFragment] estende a classe [Fragment] do Android. Geralmente, é 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] na 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 é recuperada através do seu ID;
  • linha 29: o texto é exibido no [TextView];
    • [getString] é um método da classe pai [AppCompatActivity];
    • o primeiro argumento é um ID de componente. [R.string.section_format] refere-se ao ID 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)
    • na 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 na linha (d) acima;
    • [getArguments] devolve a referência ao conjunto de argumentos do fragmento. É importante notar aqui que cada argumento foi criado com o seguinte conjunto (linhas f-h):

    // renders a fragment with one piece of information: the fragment number passed as a parameter
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      // on-board info
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // result
      return fragment;
}
  • (continuação)
    • getArguments().getInt(ARG_SECTION_NUMBER) irá, portanto, devolver o valor [sectionNumber] das linhas (g) e (b) acima;
  • linha 31: devolvemos 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 item de 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 acima corresponde à seguinte imagem (o menu encontra-se no canto superior direito da janela do ambiente de execução do Android):

 

Este menu é tratado da seguinte forma na atividade [MainActivity]:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }
 
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in 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 quaisquer opções;
  • linha 4: é utilizado o ficheiro [res/menu/menu_main.xml]. Ao objeto [Menu menu] passado como parâmetro são atribuídas as opções de menu definidas neste ficheiro;
  • linha 5: é indicado que o menu foi criado;
  • linhas 8–21: o método [onOptionsItemSelected] é executado sempre que uma opção do menu é clicada;
  • linha 13: a referência da opção de menu clicada;
  • linhas 16–18: se a opção clicada for aquela com o identificador [action_settings], nada é feito e é indicado que o evento foi tratado (linha 17);
  • linha 20: o evento é passado para a classe pai;

Para ver 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");
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }
 
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();
 
    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      Log.d("menu", "action_settings selected");
      return true;
    }
    // parent
    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 pela estrutura do Android, especificamente um envelope.

Este componente é tratado na classe [MainActivity] da seguinte forma:


    // floating button
    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: recuperamos a referência do botão flutuante na vista associada à atividade (activity_main);
  • linhas 3–9: atribuímos-lhe um manipulador para tratar os cliques nele;
  • linha 6: a classe [Snackbar] permite exibir mensagens temporárias na vista utilizando o seu método [Snackbar.make]. O primeiro argumento é uma vista a partir da qual [Snackbar] irá procurar uma vista pai na qual exibir a mensagem. Aqui, [view] é a vista do envelope clicado (linha 5). A vista pai que será encontrada é 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: pode clicar na mensagem exibida para acionar uma ação. Aqui, nenhuma ação está associada ao clique na mensagem. Por fim, o método [show] exibe a mensagem;

Clicar no botão flutuante produz o seguinte resultado visual:

 

1.7.5. Executar o projeto

Agora que explicámos os detalhes do código gerado, podemos compreender melhor a sua execução:

Image

Quando clica no separador #i, o fragmento #i é apresentado no contentor de visualização. Isto é evidente pelo texto apresentado em [4]. Também pode ver que é possível alternar entre separadores deslizando a visualização para a direita ou para a esquerda com o rato. Veremos que este comportamento pode ser controlado.

Quando clica na opção de menu em [6], obtém os seguintes registos:

 

1.7.6. Ciclo de vida do fragmento

  • Em [1], vemos que o método [onCreateView] e os métodos subsequentes são executados quando o fragmento é exibido pela primeira vez e sempre que a atividade precisa de o redesenhar;

Para acompanhar o ciclo de vida da atividade e dos fragmentos, adicionamos os seguintes registos ao código [MainActivity]:


// manufacturer
  public MainActivity(){
    Log.d("MainActivity","constructor");
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity","onCreate");
      // parent
    super.onCreate(savedInstanceState);
...
  }
 
  // a fragment
  public static class PlaceholderFragment extends Fragment {
    // a text displayed in the fragment
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    public PlaceholderFragment() {
      Log.d("PlaceholderFragment", "constructor");
    }
 
    // renders a fragment with one piece of information: the fragment number passed as a parameter
    public static PlaceholderFragment newInstance(int sectionNumber) {
      Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
      // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      ...
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
      Log.d("PlaceholderFragment", String.format("newInstance %s", getArguments().getInt(ARG_SECTION_NUMBER)));
      ...
    }
  }
 
 
}

Executamos o projeto novamente. Os primeiros registos são os seguintes:

1
2
3
4
5
6
7
8
9
05-28 10:44:32.622 29371-29371/exemples.android D/MainActivity: constructor
05-28 10:44:32.626 29371-29371/exemples.android D/MainActivity: onCreate
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: newInstance 1
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: newInstance 2
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 10:44:32.760 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 10:44:33.349 29371-29371/exemples.android D/menu: création menu en cours
  • 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: instanciamento 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;

Aqui, devemos recordar o código responsável pela criação dos fragmentos:


  // the fragment manager
  // it is used to request fragments to be displayed in the main view
  // must define methods [getItem] and [getCount] - the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }
 
    // fragment n° position
    @Override
    public Fragment getItem(int position) {
      // instantiate a fragment [PlaceHolder] and render it
      return PlaceholderFragment.newInstance(position + 1);
    }
...
  • linhas 11–15: um fragmento é instanciado por [newInstance] sempre que o contêiner de fragmentos solicita um;

Os registos acima mostram que os dois primeiros fragmentos foram instanciados e inicializados.

Agora, vamos clicar no separador n.º 2. Os novos registos são os seguintes:

1
2
3
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: newInstance 3
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 3
  • Linhas 1–3: O Fragmento n.º 3 é instanciado e inicializado. Lembre-se de que o Fragmento n.º 2 é o que está a ser exibido;

Agora, vamos clicar no separador n.º 3. Aqui não há registos. Isto deve-se provavelmente ao facto de o fragmento n.º 3, que deve ser apresentado, já ter sido instanciado. Agora, vamos voltar ao separador n.º 1. Os registos são os seguintes:

05-28 10:48:26.630 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 1

O fragmento n.º 1 não é instanciado novamente, mas o seu método [onCreateView] é executado novamente. Este comportamento ocorre também nos outros dois fragmentos.

A partir destes registos, podemos concluir que:

  • a atividade foi instanciada e inicializada uma vez;
  • cada fragmento foi instanciado uma vez;
  • o método [onCreateView] de cada fragmento foi executado várias vezes;

O que precisa de saber — e o que os registos confirmam — é que, por predefinição, quando o fragmento #i é apresentado, os fragmentos i-1 e i+1 são instanciados, caso ainda não o estejam. Isto explica, por exemplo, porque é que, no arranque, apesar de o fragmento #1 dever ser apresentado, os fragmentos 1 e 2 foram instanciados e inicializados. Os registos também mostram que o método [getItem(i)] é chamado apenas uma vez, mesmo que o fragmento #i seja exibido várias vezes. Assim, parece que o contentor de fragmentos [ViewPager], que deve exibir o fragmento #i do [SectionsPagerAdapter], o solicita uma vez ao gestor de fragmentos [ ]. Depois disso, não o solicita novamente e continua a usar aquele que obteve.

Por fim, os registos fornecem informações sobre o método [onCreateView] dos fragmentos:

  • no arranque, os fragmentos 1 e 2 foram instanciados e o seu método [onCreateView] executado;
  • Ao alternar do Fragmento 1 para o Fragmento 2, o método [onCreateView] do Fragmento 2 não é reexecutado. Portanto, não pode ser utilizado para atualizar o Fragmento 2. No entanto, o utilizador pode ter realizado uma operação no Fragmento 1 cujo resultado deveria ser apresentado pelo Fragmento 2. Vemos que o método [onCreateView] não pode ser utilizado para atualizar o Fragmento 2. Teremos de encontrar outra solução;

1.8. Exemplo-07: Exemplo-06 reescrito utilizando a biblioteca [AA]

1.8.1. Criação do projeto

Vamos duplicar o projeto [Exemplo-06] para [Exemplo-07] para introduzir anotações Android neste último. Para tal, siga o procedimento descrito na secção 1.4. Obtemos o seguinte resultado:

1.8.2. Configuração do Gradle

 

Atualizamos o ficheiro [build.gradle] da seguinte forma:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}
 
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
 
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    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 secção 1.4).

1.8.3. Adicionar as primeiras anotações AA

Vamos criar anotações AA em [MainActivity]:

  

A classe [MainActivity] é alterada da seguinte forma:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
  // the tab manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // the floating button
  @ViewById(R.id.fab)
  protected FloatingActionButton fab;
 
 
  // manufacturer
  public MainActivity() {
    Log.d("MainActivity", "constructor");
  }
 
  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
 
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
 
    // the fragment manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
 
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i issued by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // the tab bar is also associated with the fragment container
    // i.e. tab n° i displays fragment n° i of the container
    tabLayout.setupWithViewPager(mViewPager);
 
    // floating button
    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] torna [MainActivity] uma classe gerida pelo 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] é injetado no campo [tabLayout]. Este é o gestor de separadores;
  • linhas 14–15: o componente identificado por [R.id.fab] é injetado no campo [fab]. Este é o botão flutuante;
  • linhas 23–50: o código que anteriormente se encontrava no método [onCreate] é movido para um método com qualquer nome, mas anotado com [@AfterViews] (linha 23). No método anotado desta forma, podemos ter a certeza de que todos os componentes da interface visual anotados com [@ViewById] foram inicializados;
  • Também adicionámos registos para visualizar o ciclo de vida da atividade;

Lembre-se de que a anotação [@EActivity] irá gerar uma classe [MainActivity_], que será a atividade real do projeto. Portanto, deve modificar 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.

Neste ponto, execute o projeto novamente e verifique se ainda obtém a interface com separadores.

1.8.4. Reescrevendo os fragmentos

Vamos rever como os fragmentos são geridos no projeto. Por enquanto, a classe [PlaceholderFragment] é uma classe interna estática da atividade [MainActivity]. Voltaremos a um caso de uso mais comum, em que os fragmentos são definidos em classes externas. Além disso, estamos a introduzir anotações AA para fragmentos.

O projeto [Example-07] evolui da seguinte forma:

  

Acima, vemos a classe [PlaceholderFragment], que foi movida para fora da classe [MainActivity]. Ela foi 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;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
 
  // fragment no
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // manufacturer
  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)));
    // parent
    super.onResume();
    // display
    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: insira no campo [textViewInfo] a referência ao componente em [fragment_main.xml] identificado por [R.id.section_label], que é do 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 de a vista associada ao fragmento ser apresentada. Pode ser utilizado para atualizar a interface do utilizador que será apresentada;
  • linha 47: deve chamar o método com o mesmo nome na classe pai;
  • linha 49: Não é claro se o método [onResume] pode ser executado antes de o campo na linha 20 ser inicializado. Os registos configurados para acompanhar o ciclo de vida do fragmento irão indicar-nos isso. Por enquanto, como precaução, realizamos uma verificação de nulo;
  • Linha 51: Atualizamos as informações no campo [textViewInfo] com o argumento inteiro passado para o fragmento durante a sua criação;

A classe [MainActivity] perde a sua classe interna [PlaceholderFragment] e vê o seu gestor de fragmentos evoluir da seguinte forma:


public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private Fragment[] fragments;
    // number of fragments
    private static final int FRAGMENTS_COUNT = 3;
    // fragment no
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
      // initialization of fragment table
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length; i++) {
        // create a fragment
        fragments[i] = new PlaceholderFragment_();
        // you can pass arguments to the
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
    }
 
    // fragment n° position
    @Override
    public Fragment getItem(int position) {
        Log.d("MainActivity", String.format("getItem[%s]", position));      
      return fragments[position];
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      return fragments.length;
    }
 
    // optional - gives a title to managed fragments
    @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 matriz de fragmentos é inicializada 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 irá gerar uma classe [PlaceholderFragment_] derivada de [PlaceholderFragment], e é esta 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. Uma vez que estes são também os títulos dos separadores, devemos ver uma alteração na barra de separadores;

Vamos compilar [Make] [1] este projeto:

 
  • em [2], podemos ver que as classes geradas pela biblioteca AA estão localizadas na pasta [app / build / generated / source / apt / debug] (tem de estar na perspetiva [Project] para ver [2]);

Execute o projeto [Exemplo-07] e verifique se continua a funcionar.

1.8.5. Análise dos registos

Quando a aplicação é iniciada, os registos são os seguintes:

05-28 13:54:54.801 8809-8809/exemples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/exemples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/exemples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/exemples.android D/menu: création menu en cours
  • linha 1: construção da atividade única;
  • linha 2: o método [afterViews] da atividade: os seus campos anotados com [@ViewById] são inicializados;
  • linhas 3-5: construção dos três fragmentos;
  • linhas 6-7: o contêiner de fragmentos [ViewPager] solicita os dois primeiros fragmentos;
  • linhas 8-9: métodos do fragmento 2;
  • linhas 10–11: métodos do fragmento 1;
  • linhas 12–13: método [onResume] do fragmento 1;
  • linhas 14–15: método [onResume] do fragmento 2;
  • linha 16: criação do menu da atividade;

Note que isto responde a uma pergunta feita anteriormente: o método [onResume] do fragmento 1, por exemplo (linha 12), é executado após o método [afterViews] do fragmento (linha 11). Portanto, quando o método [onResume] é executado, pode utilizar os campos anotados com [@ViewById]. Podemos agora escrever o método [onResume] da seguinte forma:


  @Override
  public void onResume() {
    Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // parent
    super.onResume();
    // display
    textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}

Agora vamos mudar do separador 1 para o separador 2. Os novos registos são os seguintes:

1
2
3
4
5
05-28 14:01:42.786 8809-8809/exemples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3
  • linha 1: o contêiner de fragmentos [ViewPager] solicita o fragmento nº 3;
  • linhas 2-3: métodos do fragmento n.º 3. Note-se que este fragmento foi instanciado quando a aplicação foi iniciada;
  • linhas 4-5: o método [onResume] do fragmento n.º 3 é executado. Note-se que o fragmento n.º 2 está atualmente a ser apresentado;

Agora vamos mudar do separador 2 para o separador 3. Não há registos. Portanto, nenhum dos métodos [onCreateView, afterViews, onResume] do fragmento n.º 3 é executado. Ele exibe corretamente o texto [Hello World da secção:3] apenas porque este texto já tinha sido criado na etapa anterior, quando o Fragmento n.º 2 foi exibido. Lembre-se de que, nessa etapa, o método [onResume] do Fragmento #3 já tinha sido executado. Podemos ver aqui que, tal como o método [onCreateView], o método [onResume] não pode ser utilizado para atualizar o Fragmento 3. Se tivéssemos precisado de alterar o texto exibido pelo fragmento, nenhum destes dois métodos o teria conseguido fazer.

Agora, vamos voltar da guia #3 para a guia #1. Os registos são então os seguintes:

1
2
3
4
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1

Podemos ver que todos os métodos no Fragment 1 foram executados. Podemos ver que o método getItem não foi chamado. Como mencionado, este método é chamado apenas uma vez para cada fragmento;

Agora, vamos mudar do separador 1 para o separador 2 adjacente. Obtemos os seguintes registos:

1
2
3
4
05-28 14:12:59.526 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

Surpreendente, não é? Todos os métodos do fragmento n.º 3 são reexecutados.

Para compreender estes fenómenos, lembre-se de que, por predefinição, quando o contêiner de fragmentos exibe o fragmento i, ele inicializa os fragmentos i-1, i e i+1. Vamos rever os registos à luz desta informação.

Primeiro, os registos quando a aplicação é iniciada:

05-28 13:54:54.801 8809-8809/exemples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/exemples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/exemples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/exemples.android D/menu: création menu en cours

Como o contêiner de fragmentos exibirá o fragmento 1, os fragmentos 1 e 2 são inicializados (linhas 8–15).

Passamos agora da guia 1 para a guia 2:

1
2
3
4
5
05-28 14:01:42.786 8809-8809/exemples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

Como o contêiner de fragmentos exibirá o fragmento 2, os fragmentos 1, 2 e 3 devem ser inicializados. Os fragmentos 1 e 2 já foram inicializados na etapa anterior. O fragmento 3 é inicializado nas linhas 2–5.

Mudamos do separador 2 para o separador 3. Não há registos. Como o contentor de fragmentos irá apresentar o fragmento 3, os fragmentos 2 e 3 têm de ser inicializados. No entanto, desde a etapa anterior, já o estão. O que não vemos aqui é que o fragmento 1, que não é adjacente ao fragmento 3, perde o seu estado, que não é retido na memória.

Mudamos do separador 3 para o separador 1. Os registos são os seguintes:

1
2
3
4
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1

Como o contêiner de fragmentos exibirá o fragmento 1, o fragmento 2 também deve ser inicializado. Ele já foi inicializado na etapa anterior. Nessa mesma etapa, o estado do fragmento 1 foi perdido. Por isso, ele é reinicializado nas linhas 1–4. O que não vemos aqui é que o fragmento 3, que não é adjacente ao fragmento 1, perde o seu estado, que então não é retido na memória.

Ao mudar do separador 1 para o separador 2 adjacente, obtemos os seguintes registos:

1
2
3
4
05-28 14:12:59.526 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

Como o contêiner de fragmentos exibirá o fragmento 2, os fragmentos 1, 2 e 3 devem ser inicializados. Os fragmentos 1 e 2 já foram inicializados na etapa anterior. O fragmento 3 é inicializado nas linhas 1–4.

O que aprendemos?

  • que a gestão padrão de fragmentos é muito específica e que é necessário compreendê-la se não quiser arrancar os cabelos. Podemos alterar este modo de gestão, e faremos isso um pouco mais tarde;
  • que, com este tratamento padrão, nenhum dos métodos [onCreateView, onResume] pode ser utilizado para atualizar o fragmento que será exibido, porque não podemos ter a certeza de que serão executados;

1.8.6. onDestroyView

O método [onDestroyView] faz parte do ciclo de vida do fragmento (ver secção 1.7.6):

Vemos 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] mais tarde, há necessariamente uma chamada ao método [onDestroyView] [2];

Iremos 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;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
...
 
  @Override
  public void onDestroyView() {
    // log
    Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // parent
    super.onDestroyView();
  }
 
}

Vamos executar a aplicação. Os primeiros registos são os seguintes:

06-03 02:45:42.163 2346-2346/exemples.android D/MainActivity: constructor
06-03 02:45:42.331 2346-2346/exemples.android D/MainActivity: afterViews
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.515 2346-2346/exemples.android D/MainActivity: getItem[0]
06-03 02:45:42.516 2346-2346/exemples.android D/MainActivity: getItem[1]
06-03 02:45:42.517 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 02:45:42.520 2346-2346/exemples.android D/PlaceholderFragment: afterViews 2
06-03 02:45:42.523 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:45:42.525 2346-2346/exemples.android D/PlaceholderFragment: onResume 2
06-03 02:45:42.525 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 02:45:44.596 2346-2346/exemples.android D/menu: création menu en cours
  • linha 1: construção da atividade única;
  • linha 2: o método [afterViews] da atividade: os seus campos anotados com [@ViewById] são inicializados;
  • linhas 3-5: construção dos três fragmentos;
  • linhas 6-7: o contêiner de fragmentos [ViewPager] solicita os dois primeiros fragmentos;
  • linhas 8-9: a vista para o fragmento 2 é criada (não necessariamente tornada visível);
  • linhas 10–11: a vista para o 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: o menu da atividade é criado;

Mudar 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 contêiner de fragmentos solicita o terceiro fragmento;
  • linhas 2-3: a vista para o fragmento 3 é criada (não necessariamente exibida);
  • linhas 4-5: o método [onResume] do fragmento 3 é executado;
  • linha 6: o método [onDestroyView] do fragmento 1 é executado. Isto significa que, quando o utilizador regressar ao fragmento 1 ou a um fragmento adjacente, o ciclo de vida deste fragmento será reexecutado;

Retornando da guia 3 para a guia 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 passou por um [onDestroyView];
  • linha 5: o método [onDestroyView] do fragmento 3 é agora executado. Mais uma vez, quando o utilizador regressar ao fragmento 3 ou a um fragmento adjacente, o ciclo de vida deste fragmento será executado novamente;

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;
 
....
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
 
  ...
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
  }
}

No 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 Fragment 1 fica visível. Também podemos ver que fica visível antes de o seu método [onCreateView] ser executado;

Vamos mudar 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 é exibido (linha 4);

Vamos mudar 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 é exibido (linha 2);

Vamos voltar 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 é exibido (linha 3);

O que aprendemos?

  • O método [setUserVisibleHint] é executado uma vez com a propriedade [isVisibleToUser] definida como true para o fragmento que está prestes a ser exibido;
  • Não podemos 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 deste 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] está prestes a exibir o fragmento #i, executa, se ainda não o tiver feito, o ciclo de vida dos fragmentos adjacentes i-1 e i+1. Este comportamento pode ser controlado pelo método [ViewPager].setOffscreenPageLimit:

// fragment offset
    [ViewPager].setOffscreenPageLimit(n);

Com a instrução acima,

  1. quando o contêiner de fragmentos [ViewPager] estiver prestes a exibir o fragmento #i, ele executa, caso ainda não o tenha feito, o ciclo de vida dos fragmentos adjacentes no intervalo [i-n, i+n];
  2. se o fragmento j for então exibido:
    • ocorre o mesmo fenómeno para os fragmentos adjacentes no intervalo [j-n, j+n];
    • os fragmentos inicializados na etapa 1 que já não são adjacentes ao novo fragmento dentro do intervalo [j-n, j+n] podem então ser submetidos a uma operação [onDestroyView]. No entanto, observei noutras aplicações, particularmente na do Capítulo 3, que nem sempre era esse o caso;

Modificamos o método [MainActivity.afterViews] da seguinte forma:


  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
 
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
 
    // the fragment manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
 
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i delivered by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
 
    // fragment offset
    mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);
 
    // the tab bar is also associated with the fragment container
    // i.e. tab n° i displays fragment n° i of the container
    tabLayout.setupWithViewPager(mViewPager);
 
    // floating button
    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á simultaneamente inicializar os fragmentos 2, 3, ..., n, onde n = 1 + mSectionsPagerAdapter.getCount() - 1 = mSectionsPagerAdapter.getCount(). Isto significa que todos os fragmentos serão inicializados. Quando a janela de visualização se deslocar para outro fragmento, o contentor de fragmentos:
    • detetará 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 todos os fragmentos, nenhum será «desinicializado» pelo contentor de fragmentos;

No total, devemos ver todos os fragmentos instanciados e inicializados quando a aplicação é iniciada e, depois, nunca mais. É isso que vamos agora verificar, examinando os registos.

No arranque, temos os seguintes registos:

06-03 03:30:55.411 10344-10344/exemples.android W/System: ClassLoader referenced unknown path: /data/app/exemples.android-1/lib/x86
06-03 03:30:55.417 10344-10344/exemples.android D/MainActivity: constructor
06-03 03:30:55.460 10344-10344/exemples.android D/MainActivity: afterViews
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.559 10344-10344/exemples.android D/MainActivity: getItem[0]
06-03 03:30:55.559 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/MainActivity: getItem[1]
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/MainActivity: getItem[2]
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 03:30:56.798 10344-10344/exemples.android D/menu: création menu en cours
  • linhas 4–6: construção dos três fragmentos;
  • linhas 7, 9, 11: o contêiner de fragmentos solicita os três fragmentos. Na versão anterior, solicitava dois;
  • linhas 14-25: o ciclo de vida dos três fragmentos é executado;

Agora vamos mudar do separador 1 para o separador 2:

06-03 03:34:03.388 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:34:03.388 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true

Vamos mudar do separador 2 para o separador 3:

06-03 03:34:43.292 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:34:43.292 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true

Em seguida, da guia 3 para a guia 1:

06-03 03:35:32.666 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:35:32.666 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true

Os registos confirmam a teoria. Todos os fragmentos foram instanciados e inicializados no arranque. Depois disso, os seus métodos de ciclo de vida deixam de ser executados. Este é um comportamento muito previsível dos fragmentos, o que os torna muito mais fáceis de utilizar.

O que queremos encontrar é uma forma de atualizar um fragmento que está prestes a ser exibido, 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 está prestes a ser exibido, mas não para os outros;
  • este evento pode ocorrer antes ou depois do ciclo de vida do fragmento. Isto depende da adjacência de fragmentos escolhida pelo programador. Isto é um problema porque, se o ciclo de vida ainda não tiver ocorrido, 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 do fragmento era 1, eram 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
  • Podemos ver que, quando o Fragmento 1 fica visível, a sua vista ainda não foi criada. Por isso, não podemos interagir com ele. Isto pode ser feito durante o ciclo de vida do fragmento, por exemplo, no método [onCreateView] (linha 11) ou no método [onResume] (linhas 13–14). Uma vez que estamos a utilizar anotações AA, normalmente não precisamos de escrever o método [onCreateView]. Por conseguinte, o método [onResume] parece ser o mais adequado aqui para atualizar o Fragmento 1;

Quando mudá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] na linha 4 para atualizar o fragmento 2;

Quando mudá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] na linha 2 para atualizar o fragmento 3;

Quando mudá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, deve 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 está prestes a ser exibido, temos dois métodos: [setUserVisibleHint] e [onResume].

Iremos implementar esta solução num novo projeto em que cada fragmento deve exibir o número de vezes que foi exibido, o que designaremos por «visita». Por conseguinte, teremos de atualizar a sua exibição sempre que for exibido. Este é, de facto, o problema que estamos a tentar resolver.

Antes disso, vamos examinar a fase final do ciclo de vida de uma atividade ou fragmento: quando é destruído. O sistema pode decidir destruir uma atividade se outras atividades com prioridade mais elevada necessitarem de recursos que estão atualmente indisponíveis. Para libertar esses recursos, o sistema tomará a iniciativa de destruir determinadas atividades. O método [onDestroy] da atividade e dos fragmentos será então chamado.

1.8.9. OnDestroy

Permitiremos que o utilizador elimine a atividade utilizando uma opção do menu [5]. Para tal, adicionamos uma nova opção de menu ao 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 rótulo para esta 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], tratamos do clique na opção [Terminar]:


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in 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");
      //we finish the activity
      finish();
      return true;
    }
    // parent
    return super.onOptionsItemSelected(item);
}
  • linhas 14–19: copie e cole as linhas 10–13 e adapte o código à nova opção;
  • linha 17: a atividade é encerrada por uma ação do software;

Agora vamos executar esta nova versão e, assim que a primeira vista for apresentada, clique na opção de menu [Terminar]. Os registos ficam então da seguinte forma:

1
2
3
4
5
6
7
8
9
06-04 12:35:32.996 15994-15994/exemples.android D/menu: onOptionsItemSelected
06-04 12:35:32.996 15994-15994/exemples.android D/menu: action_terminate selected
06-04 12:35:33.561 15994-15994/exemples.android D/MainActivity: onDestroy
06-04 12:35:33.561 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 1
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 1
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 2
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 2
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 3
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 3
  • linhas 1-2: clique na opção [Terminar];
  • linha 4: o método [onDestroy] da atividade é chamado;
  • linhas 4-5: o método [onDestroyView] do fragmento 1 é chamado, seguido pelo seu método [onDestroy];
  • linhas 6-9: este processo repete-se para os outros dois fragmentos;

É importante lembrar que o método [onDestroy] da atividade e dos fragmentos é chamado quando a atividade está prestes a ser destruída pelo sistema, pelo programador ou pelo utilizador. Este método pode ser utilizado para guardar informações — por exemplo, localmente no tablet — para que possam ser recuperadas quando o utilizador reiniciar a aplicação.

1.9. Exemplo-08: Atualização de um fragmento com adjacência variável de fragmentos

1.9.1. Criação do projeto

Duplique o projeto [Exemplo-07] para [Exemplo-08]. Para tal, siga o procedimento descrito para duplicar [Exemplo-02] para [Exemplo-03] na secção 1.4.

1.9.2. Reescrever o fragmento [PlaceholderFragment]

O novo código para o 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;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  // data
  private boolean afterViewsDone = false;
  private boolean initDone = false;
  private String text;
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private int numVisit = 0;
 
  // fragment no
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // manufacturer
  public PlaceholderFragment() {
    Log.d("PlaceholderFragment", "constructor");
  }
 
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    if (!initDone) {
      // initial text
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // init done
      initDone = true;
    }
    // current text display
    textViewInfo.setText(text);
  }
 
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }
 
  @Override
  public void onDestroyView() {
...
  }
 
  @Override
  public void onResume() {
...
  }
 
  // update fragment
  public void update() {
    // the work to be done depends on the visit number
    if (numVisit > 1) {
      // log
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // modified text
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }
 
  // local info for logs
  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. Costumávamos usá-lo para inicializar o texto do fragmento (linha 42). Continuamos a fazê-lo, mas para garantir que isso aconteça apenas uma vez, gerimos um booleano [initDone] (linha 44) para indicar que a inicialização foi concluída e não precisa de ser repetida;
  • linhas 56–59: Introduzimos o método [onDestroyView] para ter em conta 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. O método [setUserVisibleHint], no entanto, nem sempre é executado após o método [@AfterViews]. Os registos mostraram que pelo menos um dos dois é executado após o método [@AfterViews]. Os registos nunca mostraram que ambos pudessem ser executados em conjunto após o método [@AfterViews]. É um ou outro. Por precaução, definiremos um booleano [updateDone] quando for feita uma atualização;

Os métodos [setUserVisibleHint] e [onResume] são os seguintes:


  // data
  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) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // memory
    this.isVisibleToUser = isVisibleToUser;
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // number of visits
    if (isVisibleToUser) {
      // increment
      numVisit++;
      // update fragment
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // the fragment will be hidden
      updateDone = false;
    }
  }
 
  @Override
  public void onResume() {
    // parent
    super.onResume();
    // log
    Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // update
    if (isVisibleToUser && !updateDone) {
      update();
      updateDone = true;
    }
}
  • linha 14: o estado de visibilidade do fragmento é guardado;
  • 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] é definida como true;
  • linhas 26–28: se o fragmento for ocultado, o booleano [updateDone] é redefinido para false. Precisamos de um evento para redefinir o booleano [updateDone] — que é definido como true assim que o método [update] é chamado — para false, para que novas atualizações possam ser feitas. Utilizamos o facto de o fragmento já não estar visível para fazer isto. Quando se torna visível novamente, o fragmento deve ser atualizado mais uma vez;
  • 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 realizamos a atualização (linha 39) e, tal como fizemos para [setMenuVisibility], gerimos a variável booleana [updateDone].

Por fim, o método [onDestroyView] é o seguinte:


  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    // indicator update
    afterViewsDone = false;
    // log
    Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}

O método [onDestroyView] é executado quando o ciclo de vida de um fragmento termina. Outro ciclo de vida pode ser retomado mais tarde.

  • Linha 6: O método [onDestroyView] remove qualquer ligação à vista associada ao fragmento. Esta será recriada durante o próximo ciclo de vida do fragmento. Por enquanto, precisamos de definir o booleano [afterViews] como false para indicar que a ligação à 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]:


    // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
  private final int OFF_SCREEN_PAGE_LIMIT=2;
 
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
   @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
 
    ....
 
    // fragment offset
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
 
...
}

Os registos de 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 contêiner de fragmentos solicita todos os fragmentos adjacentes ao fragmento 1;
  • linhas 9, 11, 13: o método [setUserVisibleHint] destes fragmentos é executado com [visibleToUser] definido como false;
  • Linha 14: O método [setUserVisibleHint] do fragmento 1 é chamado com [visibleToUser] definido como true;
  • linhas 15–17: o método [afterViews] dos 3 segmentos adjacentes é chamado. Aqui vemos um caso em que este método é chamado depois de um fragmento se ter tornado visível (Fragmento 1, linha 14);
  • linhas 18–20: o método [onResume] dos 3 segmentos adjacentes é chamado;

Mudança da guia 1 para a guia 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
  • porque o layout do fragmento é deslocado uma posição para a direita, o fragmento 4 é reivindicado pelo contêiner de fragmentos;
  • linha 2: o método [setUserVisibleHint] do fragmento 4 é chamado com [visibleToUser] definido como false;
  • linha 3: o método [setUserVisibleHint] do fragmento 1 é chamado com [visibleToUser] definido como false. Como resultado, o fragmento 1 fica agora oculto;
  • linha 4: o método [setUserVisibleHint] do fragmento 2 é chamado com [visibleToUser] definido como true. O fragmento 2 está agora visível;
  • linhas 5-6: o ciclo de vida do fragmento 4 continua;

Mudamos 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
  • porque o layout do fragmento é deslocado uma posição para a direita, o fragmento 5 é reivindicado pelo contêiner de fragmentos;
  • linha 2: o método [setUserVisibleHint] do fragmento 5 é chamado com [visibleToUser] definido como false;
  • linha 3: o método [setUserVisibleHint] do fragmento 2 é chamado com [visibleToUser] definido como false. Como resultado, o fragmento 2 fica agora oculto;
  • linha 4: o método [setUserVisibleHint] do fragmento 3 é chamado com [visibleToUser] definido como true. O fragmento 3 está agora visível;
  • linhas 5-6: o ciclo de vida do fragmento 5 continua;

Mudamos 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 o ciclo de vida do fragmento 4 não é executado. Isto já foi feito duas etapas antes;
  • linha 3: o fragmento 1 sai da vizinhança do fragmento 4 exibido. O seu método [onDestroyView] é executado. Da próxima vez que for exibido, o seu ciclo de vida de visualização [onCreateView, afterViews, onResume] será reexecutado;

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 o ciclo de vida do fragmento 5 não é executado. Isto já foi feito duas etapas antes;
  • linha 3: o fragmento 2 sai da vizinhança do fragmento 5 exibido. O seu método [onDestroyView] é executado;

Mudamos 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. Isto deve-se ao facto de ter perdido a ligação à 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 são removidos da vizinhança do fragmento exibido;
  • linha 7: o fragmento 1 é atualizado;
 

Os registos nunca mostraram que os métodos [setUserVisibleHint] e [onResume] tentaram ambos atualizar o fragmento. É um ou outro. O leitor é convidado a realizar mais testes e a monitorizar os registos para compreender totalmente os conceitos de adjacência e ciclo de vida dos fragmentos.

Agora, vamos definir a adjacência total e executar os mesmos testes.

Em [MainActivity]:


  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;

Os registos de 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 está a ser executado;
  • O Fragmento 1 é exibido na linha 18;

Mudança da guia 1 para a guia 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 está oculto;
  • linha 2: o fragmento 2 é exibido;

Mudança 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;

Mudança da guia 3 para a guia 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;

Mudança da guia 4 para a guia 5:


05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • linha 1: o fragmento 4 está oculto;
  • linha 2: o fragmento 5 é exibido;

Passamos 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;

Mudança da guia 1 para a guia 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;

Podemos ver que, com adjacência total, o comportamento dos fragmentos é muito mais previsível.

Agora, vamos definir a adjacência como zero e ver o que acontece. A classe [MainActivity] passa a ter a seguinte forma:


  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = 0;

Os registos de 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 contêiner de fragmentos solicitou 2 fragmentos, os números 1 e 2. Tudo, portanto, prossegue como se houvesse uma adjacência de 1. A adjacência de 0 foi, portanto, ignorada.

1.9.3. Comunicação entre fragmentos

Na arquitetura anterior, temos uma atividade e n fragmentos. O utilizador interage com os vários fragmentos. Estas interações modificam o estado da aplicação. Aqui, o estado da aplicação refere-se ao conjunto de informações que esta armazena ao longo do seu ciclo de vida. Surge então o seguinte problema:

  • quando o utilizador interage com o fragmento i, a aplicação transita do estado E1 para o estado E2;
  • uma ação do utilizador no fragmento i faz com que o fragmento j seja apresentado;
  • como atualizamos o fragmento j com o estado atual E2 da aplicação?

A partir dos exemplos anteriores, sabemos como atualizar o fragmento j. Mas onde encontramos o estado E2 da aplicação para o atualizar?

Existem diferentes soluções para este problema. Já vimos uma: o fragmento i pode passar o estado E2 da aplicação para o fragmento j através de argumentos. Encontrámos este método na classe [MainActivity] ao criar os fragmentos:


      for (int i = 0; i < fragments.length; i++) {
        // create a fragment
        fragments[i] = new PlaceholderFragment_();
        // you can pass arguments to the
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
}

Esta solução não é imediatamente utilizável aqui. Na verdade, quando o utilizador clica no separador j, o que irá apresentar o fragmento j, o nosso código não é chamado. Apenas o código do sistema é executado. Veremos num projeto futuro como interceptar um clique no separador, mas, por agora, vamos adotar uma abordagem diferente.

Já discutimos o estado da aplicação: o conjunto de dados geridos pela aplicação ao longo do tempo. Aqui, a aplicação consiste numa atividade e em n fragmentos, todos instanciados uma vez no arranque da aplicação e cujo tempo de vida corresponde ao da aplicação. Portanto, qualquer um destes elementos — ou vários deles em conjunto — pode servir como candidato para armazenar o estado da aplicação. Cada fragmento tem acesso, através do método [Fragment.getActivity()], à atividade que o criou. Uma vez que todos os fragmentos têm acesso à atividade, parece natural armazenar o estado da aplicação dentro dela.

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 à classe [PlaceholderFragment]:


  // update fragment
  public void update() {
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // the work to be done depends on the visit number
    if (numVisit > 1) {
      // log
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // modified text
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }
 
  // local info for logs
  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 [getInfo] exibe parte do estado da aplicação;

Iniciamos a aplicação com uma adjacência de fragmentos igual a 2. Os registos quando a aplicação é iniciada:


06-01 03:26:13.769 10931-10931/exemples.android D/MainActivity: constructor
06-01 03:26:13.856 10931-10931/exemples.android D/MainActivity: afterViews
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:14.535 10931-10931/exemples.android D/MainActivity: getItem[0]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[1]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[2]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.541 10931-10931/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.545 10931-10931/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:15.967 10931-10931/exemples.android D/menu: création menu en cours
  • linhas 9, 10, 13, 14: vemos que nos métodos [setUserVisibleHint], temos [getActivity()==null] se o fragmento ainda não estiver visível (isVisibleToUser==false);
  • linha 19: vemos que, quando o fluxo de execução chega ao método [update] do fragmento 1, o método [getActivity] retorna corretamente a atividade;

Quando a adjacência do fragmento é definida como 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

Obtemos os mesmos resultados. Podemos concluir que, assim que o fragmento fica visível, o método [getActivity] devolve a atividade do fragmento. Observamos também que, quando a execução chega ao método [update] do fragmento que está prestes a ser exibido, o método [getActivity] devolve efetivamente um valor.

Para ilustrar a comunicação entre fragmentos, estamos a criar um novo projeto.

1.10. Exemplo-09: Comunicação entre fragmentos, deslizar e percorrer

1.10.1. Criação do projeto

Duplicamos o projeto [Exemplo-07] para [Exemplo-08]. Para tal, seguiremos o procedimento descrito para duplicar [Exemplo-02] para [Exemplo-03] na secção 1.4.

1.10.2. A sessão

Neste novo projeto, queremos que os fragmentos exibam o número total de fragmentos visualizados pelo utilizador. Aqui, precisamos de manter um contador que seja acessível a todos os fragmentos. Vamos chamar ao objeto que encapsula os dados partilhados pelos fragmentos de «sessão». Esta terminologia provém do desenvolvimento web, onde os dados a serem partilhados entre diferentes visualizações solicitadas pelo mesmo utilizador são colocados numa sessão. Encapsular as informações partilhadas pelos diferentes fragmentos num único objeto torna o código mais legível.

A classe [Session] será a seguinte:

  

package exemples.android;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
 
  // getters and 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] especifica o âmbito (ou tempo de vida) da classe anotada. Aqui, o atributo [scope = EBean.Scope.Singleton] torna a classe [Session] um singleton: será instanciada uma única vez quando a aplicação for iniciada. Uma referência a uma classe anotada com [EBean] pode então ser injetada noutra classe. Este é o conceito de injeção de dependências;

1.10.3. A [MainActivity]

A atividade [MainActivity] evolui da seguinte forma:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  ...
 
  // injection session
  @Bean(Session.class)
  protected Session session;
 
  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
  private final int OFF_SCREEN_PAGE_LIMIT = 2;
 
    @AfterInject
  protected void afterInject(){
    Log.d("MainActivity", "afterInject");
 
    // session initialization
    session.setNumVisit(0);
  }
 
...
  • linhas 7-8: injeção da referência ao singleton de sessão utilizando a anotação [@Bean]. O parâmetro da anotação é a classe do bean a ser injetado. O campo anotado desta forma não pode ter escopo [private];
  • linha 15: a anotação [@AfterInject] é utilizada para designar um método a ser chamado assim que todas as injeções para a classe tiverem sido concluídas. Assim, ao entrar no método [afterInject] na linha 16, a referência da linha 8 já foi inicializada;
  • linha 20: o contador de visitas é reiniciado para zero;

1.10.4. O fragmento [PlaceholderFragment]

O fragmento [PlaceholderFragment] evolui da seguinte forma:


@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
....
 
  // session
  protected Session session;
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // memory
    this.isVisibleToUser = isVisibleToUser;
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // number of visits
    if (isVisibleToUser) {
      // update fragment
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // the fragment will be hidden
      updateDone = false;
    }
  }
 
  // update fragment
  public void update() {
    // log
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // session
    if (session == null) {
      session = ((MainActivity) getActivity()).getSession();
    }
    // increment visit no
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }
  • linha 7: a sessão;
  • linhas 35-37: sabemos que, quando entramos no método [update], o método [getActivity] devolve corretamente a atividade. Aproveitamos esta oportunidade para recuperar a sessão e armazená-la localmente (linha 36);
  • linhas 39–41: para incrementar o número de visitas, recuperamo-lo da sessão. Poderíamos ter colocado este código no método [setUserVisibleHint] a partir da linha 19, uma vez que sabemos que o método [getActivity] devolve a atividade nesse ponto. Aqui, decidimos não atribuir uma função específica a este método e mover o código específico do fragmento para o método [update] do fragmento, que foi concebido para esse fim;
  • linha 43: exibe o número de visitas;

Ao executar esta aplicação com 5 fragmentos, com 2 fragmentos adjacentes, 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: Podemos ver que o método [afterInject] da atividade é executado antes do seu método [afterViews];

Convidamos os leitores a testarem esta nova aplicação.

1.10.5. Desativar o deslizar

Na aplicação anterior, quando desliza o emulador Android com o rato para a esquerda ou para a direita, a vista atual é substituída pela vista à direita ou à esquerda, conforme apropriado. Este comportamento padrão nem sempre é desejável. Vamos aprender a desativar o deslizamento de vistas.

Voltemos à vista XML principal [activity_main]:

  

No código XML da vista, encontramos o contêiner de fragmentos:


  <android.support.v4.view.ViewPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

A linha 1 especifica 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 {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container
  @ViewById(R.id.container)
  protected ViewPager mViewPager;
...

Linha 12: O contêiner do fragmento é do tipo [android.support.v4.view.ViewPager] (linha 1). Para desativar o deslizar, precisamos estender 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 {
 
  // swipe control
  private boolean isSwipeEnabled;
 
  // manufacturers
  public MyPager(Context context) {
    super(context);
  }
 
  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }
 
  // methods to be redefined to manage swiping
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // swipe allowed?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }
 
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // swipe allowed?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }
 
  // setter
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }
 
}
  • linha 8: a classe [MyPager] estende a classe [ViewPager] do Android (linha 4);
  • ao deslizar com o dedo, os manipuladores de eventos nas linhas 24 e 34 podem ser chamados. Ambos devolvem um valor booleano. Basta que devolvam o valor booleano [false] para desativar o deslizar;
  • linha 11: o valor booleano usado para indicar se o gesto de deslizar deve ou não ser permitido.

Feito isto, temos agora de utilizar o nosso novo gestor de páginas. Isto é feito na vista XML [activity_main.xml] e na atividade principal [MainActivity]. Em [activity_main.xml] escrevemos:

  

  <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"/>

Linha 1: Utilizamos a nova classe. Em [MainActivity], o código altera-se da seguinte forma:


package exemples.android;
 
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
 
  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
...
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i issued by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
    // the tab bar is also associated with the fragment container
...
  • linha 12: o gestor de páginas é agora do tipo [MyPager];
  • linha 23: ativamos ou desativamos o deslizar.

Teste esta nova versão. Ative ou desative o deslizar e observe a diferença no comportamento das vistas quando as arrasta para a direita ou para a esquerda com o rato. Em todas as aplicações futuras, o deslizar estará desativado. Não voltaremos a referi-lo.

1.10.6. Desativar a rolagem entre fragmentos

Vamos continuar com uma melhoria no gestor de separadores. Ao mudar do separador 1 para o separador 4, vê os dois separadores intermédios, 2 e 3, a passar. Na gíria do Android, isto chama-se smoothScrolling. Este comportamento pode tornar-se irritante se houver muitos separadores. Pode ser desativado adicionando o seguinte código ao gestor de fragmentos [MyPager]:


// swipe control
  private boolean isSwipeEnabled;
  // controls scrolling
  private boolean isScrollingEnabled;
 
 ...
  // scrolling
  @Override
  public void setCurrentItem(int position){
    super.setCurrentItem(position,isScrollingEnabled);
  }
 
  // setters
...
 
  public void setScrollingEnabled(boolean scrollingEnabled) {
    isScrollingEnabled = scrollingEnabled;
  }

Como o gestor de separadores foi associado ao gestor de fragmentos [MyPager], quando o separador #i é clicado, o fragmento #i é apresentado pelo contentor de fragmentos utilizando o método [setCurrentItem] acima (linha 9). [position] é o número do fragmento a apresentar;

  • linha 10: o método [setCurrentItem] da classe pai é chamado. O segundo argumento definido como [false] solicita uma transição imediata entre os fragmentos antigo e novo (sem deslocamento); definido como [true] solicita uma transição através de deslocamento. Aqui, o segundo argumento é o valor do campo na linha 4, um campo que o programador pode definir utilizando o método nas linhas 16–18;

Se pretender desativar a rolagem, a classe [MainActivity] terá o seguinte aspeto:


...
    // fragment offset
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
 
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
 
    // no scrolling
    mViewPager.setScrollingEnabled(false);
...

Execute o projeto novamente e verifique se já não há rolagem entre as guias 1 e 4, por exemplo. A partir de agora, 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, copie a vista [vue1.xml] do projeto [Example-04] para o projeto [Example-09] [1]:

 
  • em [1], a vista [vue1.xml];
  • em [3], a vista apresenta erros devido à falta de texto no ficheiro [res/values/strings.xml];

Em [2], adicionamos o texto em falta, obtendo-o do ficheiro [res/values/strings.xml] no projeto [Example-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>
  <!-- vue 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–9;

Agora, criamos a classe [Vue1Fragment], que será o fragmento responsável por exibir a vista [vue1.xml]:

  

A classe [Vue1Fragment] será a seguinte:


package exemples.android;
 
import android.support.v4.app.Fragment;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends Fragment {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
}
 
  • linha 10: a anotação [@EFragment] garante que o fragmento utilizado pela atividade será efetivamente a classe [Vue1Fragment_]. Tenha isto em mente. O fragmento está associado à vista [vue1.xml];
  • linhas 14–15: o componente identificado por [R.id.editTextNom] é injetado no campo [editTextNom] na linha 15;
  • linhas 18–20: o método [doValider] lida com 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()] recupera a atividade na qual o fragmento está localizado. Trata-se de [MainActivity], uma vez que, nesta arquitetura, temos apenas uma atividade que exibe diferentes vistas ou fragmentos;

Na classe [MainActivity], o gestor de fragmentos é implementado da seguinte forma:


public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private Fragment[] fragments;
    // fragment no
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
      // initialization of fragment table
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        // create a fragment
        fragments[i] = new PlaceholderFragment_();
        // you can pass arguments to the
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
      // a fragment of +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }
 
 ...
  }
  • linha 13: existem [FRAGMENTS_COUNT] fragmentos: [FRAGMENTS_COUNT-1] fragmentos do tipo [PlaceholderFragment] (linhas 14-21) e um fragmento do tipo [Vue1Fragment_], linha 23 (repare no sublinhado);

Compile e, em seguida, execute o projeto [Example-09]. O separador 5 deverá ter um aspeto diferente:

1.10.8. Derive todos os fragmentos da mesma classe abstrata

O novo fragmento [Vue1Fragment] também precisa de se atualizar quando é apresentado. Para tal, teremos de criar código semelhante ao criado para o fragmento [PlaceholderFragment]. Para evitar repetições, iremos extrair o que for possível para uma classe abstrata da qual todos os fragmentos da aplicação irão herdar.

Para tal, criamos um novo projeto.

1.11. Exemplo-10: Derivar todos os fragmentos de uma classe abstrata

1.11.1. Criação do projeto

Duplicamos o projeto [Exemplo-09] para [Exemplo-10]:

1.11.2. Gestão do modo de depuração

Adicionamos ao projeto a opção de mostrar ou ocultar os registos do modo de depuração. Para tal, adicionamos uma constante estática à classe [MainActivity]:


  // mode debug
public static final boolean IS_DEBUG_ENABLED = false;

1.11.3. A classe pai abstrata 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 {
 
  // private data
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private String className;
 
  // data accessible to daughter classes
  protected boolean afterViewsDone = false;
  protected boolean isDebugEnabled = true;
 
  // activity
  protected MainActivity activity;
 
  // session
  protected Session session;
 
  // manufacturer
  public AbstractFragment() {
    // init
    isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
    className = getClass().getSimpleName();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    ...
  }
 
  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    ...
  }
 
  @Override
  public void onResume() {
    // parent
    super.onResume();
    ...
  }
 
  // local news
  protected String getParentInfos() {
    return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
  }
 
  // update fragment
  protected void update() {
    ...
    // the daughter class is asked to update itself
    updateFragment();
  }
 
  protected abstract void updateFragment();
}
  • linha 7: a classe [AbstractFragment] estende a classe [Fragment] do Android;
  • Cada fragmento deve ser capaz de se atualizar. É por isso que a classe pai [AbstractFragment] exige que as suas classes filhas tenham 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 são recolhidos 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: o nome da classe instanciada é armazenado, ou seja, o nome de uma classe filha;
  • linhas 15–22: estes campos têm o atributo [protected] para que as classes filhas possam aceder-lhes. Note-se que as classes filhas não têm conhecimento da existência dos booleanos [isVisibleToUser] e [updateDone] (linhas 10–11);
  • linha 57: o método [getParentInfos] tem o atributo [protected] para que as classes filhas possam chamá-lo;

Os métodos [setUserVisibleHint, onDestroyView, onResume] permanecem os mesmos que eram na classe [PlaceholderFragment] do projeto anterior:


@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // memory
    this.isVisibleToUser = isVisibleToUser;
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
    }
    // when the fragment becomes visible
    if (isVisibleToUser) {
      // update fragment
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // we leave the fragment
      updateDone = false;
    }
  }
 
  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    // indicator update
    afterViewsDone = false;
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
    }
  }
 
  @Override
  public void onResume() {
    // parent
    super.onResume();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
    }
    if (isVisibleToUser) {
      // update
      if (!updateDone) {
        update();
        updateDone = true;
      }
    }
  }

O método [update] é o seguinte:


  // update fragment
  protected void update() {
    // recover activity and session
    if (activity == null) {
      Activity activity = getActivity();
      if (activity != null) {
        this.activity = (MainActivity) activity;
        this.session = this.activity.getSession();
      }
    }
    // the daughter class is asked to update itself
    updateFragment();
}

De acordo com o código acima, quando o método [update] de um fragmento é executado, o fragmento está visível. Isto é importante porque significa que o método [Fragment.getActivity] devolve então uma referência à atividade da aplicação (ver secção 1.10.8), que, por sua vez, fornece acesso à sessão.

  • linhas 4–10: inicializam a atividade e a sessão, caso ainda não tenham sido inicializadas;
  • linha 12: o método [updateFragment] da classe filha é chamado. Quando este método é executado, os campos [activity] e [session] aos quais tem acesso já foram inicializados;

1.11.4. A classe [PlaceholderFragment]

  

A classe [PlaceholderFragment] está estruturada da seguinte forma:


package exemples.android;
 
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
 
  // data
  private boolean initDone;
 
  // data
  private String text;
  private int numVisit;
 
  // fragment no
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // manufacturer
  public PlaceholderFragment() {
    super();
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", "constructor");
    }
  }
 
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
 ...
  }
 
  // update fragment
  public void updateFragment() {
  ...
  }
 
}
  • Linha 10: A classe [PlaceholderFragment] estende a classe [AbstractFragment]. Com esta arquitetura, escrever um fragmento envolve:
    • escrever o método [@AfterViews], que é utilizado para inicializar o fragmento durante o seu primeiro ciclo de vida ou para o reiniciar caso tenha ocorrido anteriormente um [onDestroyView]. A linha 39 é necessária para gerir corretamente o ciclo de vida do fragmento;
    • escrever o método [updateFragment], que atualiza o fragmento imediatamente antes de este ser apresentado. Este método pode utilizar a sessão da sua classe pai;
    • escrever os manipuladores de eventos do fragmento. É isto que faremos em projetos futuros;

Os métodos [@AfterViews] e [updateFragment] permanecem os mesmos que eram no projeto anterior:


@AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    if (!initDone) {
      // initial text
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // init done
      initDone = true;
    }
    // current text display
    textViewInfo.setText(text);
  }
 
  // update fragment
  public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    // increment visit no
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }
 
  // local info for logs
  protected String getLocalInfos() {
    return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
      numVisit, initDone, getActivity() == null);
  }
  • Linhas 7 e 23: Nos registos, apresentamos informações da classe pai utilizando o método herdado [getParentInfos];

1.11.5. A classe [Vue1Fragment]

  

A classe [Vue1Fragment] tem 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 {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  // data
  private int numVisit;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
    }
  }
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
  // local info for logs
  protected String getLocalInfos() {
    return String.format("numVisit=%s", numVisit);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
    // increment visit no
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // the visit number is displayed
    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. Deve, no entanto, ser escrito para definir o booleano [afterViewsDone] como verdadeiro, uma vez que esta informação é utilizada pela classe pai;
  • linhas 42–49: o método [updateFragment] consiste em exibir uma mensagem curta mostrando o número de visitas (linha 48) e incrementar esse número na sessão (linhas 44–46);

Convidamos os leitores a testarem este novo projeto.

Iremos utilizar esta arquitetura em todos os projetos futuros:

  • uma atividade e n fragmentos;
  • todos os fragmentos estendem a classe [AbstractFragment];
  • os dados a serem partilhados 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 o seguinte:


// the tab bar is also associated with the fragment container
// i.e. tab n° i displays fragment n° i of the container
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 apresenta o fragmento n.º i. Ainda não vimos o inverso: quando pedimos ao contentor de fragmentos para apresentar o fragmento n.º i, o separador n.º i é automaticamente selecionado.

Para ilustrar este comportamento, vamos adicionar as opções [Fragmento 1, Fragmento 2, ...] ao menu atual. Quando o utilizador clicar na opção [Fragmento i], vamos pedir ao contentor de fragmentos para exibir o fragmento #i. Veremos então se a guia #i foi selecionada ou não.

Este passo começa com a modificação do menu da aplicação:

 

O conteúdo do ficheiro [res/menu/menu_main.xml] é alterado 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 rótulos das opções (linhas 10, 14, 18, 22, 26) estã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>
  <!-- vue 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <string name="btn_vue2">Vue n° 2</string>
  <!-- menu -->
  <string name="fragment1">Fragment 1</string>
  <string name="fragment2">Fragment 2</string>
  <string name="fragment3">Fragment 3</string>
  <string name="fragment4">Fragment 4</string>
  <string name="fragment5">Fragment 5</string>
</resources>

O resultado visual é o seguinte:

  

O tratamento dos cliques para estas opções de menu é feito na classe [MainActivity]:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // processing menu options
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d("menu", "action_settings selected");
        }
        break;
      }
      case R.id.fragment1: {
        showFragment(0);
        break;
      }
      case R.id.fragment2: {
        showFragment(1);
        break;
      }
      case R.id.fragment3: {
        showFragment(2);
        break;
      }
      case R.id.fragment4: {
        showFragment(3);
        break;
      }
      case R.id.fragment5: {
        showFragment(4);
        break;
      }
    }
    // item processed
    return true;
  }
 
  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // change the displayed fragment
      mViewPager.setCurrentItem(i);
    }
  }
  • Linha 2: O método [onOptionsItemSelected] é chamado quando uma das opções do menu é clicada;
  • linha 8: recuperamos o ID da opção clicada;
  • linhas 9–36: os diferentes casos são tratados por uma instrução switch;
  • linhas 16–36: clicar na opção [Fragment i] chama o método [showFragment(i-1)] nas linhas 41–45;
  • linha 43: o contêiner de fragmentos é solicitado a exibir o fragmento solicitado;
  • linha 42: verificamos primeiro se isso é possível (condição 1) e se é necessário (condição 2);

Convidamos os leitores a testarem esta nova versão. Observamos que, quando solicitamos a exibição do fragmento #i, este é efetivamente exibido e o separador #i é selecionado.

Agora que vimos como funciona a associação entre separadores e fragmentos, vamos analisar outro caso: aquele em que a gestão de separadores é dissociada da gestão de fragmentos. É o que acontece, 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: Separador de separadores e fragmentos

1.12.1. Criação do projeto

Duplicamos o projeto [Exemplo-10] para [Exemplo-11]:

1.12.2. Objetivos

A nova aplicação terá dois separadores:

  • A primeira guia exibirá sempre o fragmento [View1];
  • a segunda guia exibirá um fragmento selecionado no menu;

Image

  • em [1], o fragmento [View1];
  • em [2], o fragmento [PlaceholderFragment] selecionado pelo utilizador;
  • em [3], as visitas continuam a ser contadas;

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 {
  // number of fragments visited
  private int numVisit;
  // n° fragment type [PlaceholderFragment] displayed in second tab
  private int numFragment;
 
  // getters and setters
...
}
  • Linha 10: Vamos tratar nós próprios dos cliques nas guias. Quando uma guia é clicada, precisamos de carregar o fragmento que estava a ser apresentado da última vez que foi selecionado. O campo [numFragment] irá armazenar o número do fragmento para a guia n.º 2, um número no intervalo [0, Fragments_COUNT-2]. Quando a guia n.º 2 for clicada, iremos recuperar o número do fragmento a apresentar a partir da sessão;

1.12.4. O menu

  

O menu [res / menu / menu_main.xml] sofre as seguintes alterações:


<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 irá apresentar um dos quatro fragmentos das linhas 9–24. O quinto fragmento é o fragmento [Vue1Fragment], que será sempre apresentado no separador n.º 1.

1.12.5. A classe [MainActivity]

A classe [MainActivity] deve agora gerir os separadores e a navegação entre eles, o que não fazia anteriormente. O seu código altera-se da seguinte forma:


  // the tab manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
...
@AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
    ...
 
    // no scrolling
    mViewPager.setScrollingEnabled(false);
 
    // view1 display
    mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
 
    // at the start there is only one tab
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
 
    // event manager
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // a tab has been selected - change the fragment displayed by the fragment container
        ...
      }
 
      @Override
      public void onTabUnselected(TabLayout.Tab tab) {
 
      }
 
      @Override
      public void onTabReselected(TabLayout.Tab tab) {
 
      }
    });
 
...
 
}
  • linha 17: O primeiro fragmento exibido pelo contêiner de fragmentos será o fragmento [Vue1Fragment]. Por padrão, este será o último fragmento no contêiner;
  • linhas 20–22: como não estabelecemos uma 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] na linha 3 não tem separadores;
  • linha 20: criamos a primeira guia;
  • linha 21: atribuímos-lhe um título. Nos exemplos anteriores, os títulos das separadores eram os mesmos que os títulos dos fragmentos. Isso já não é o caso. Como resultado, removemos o método [getPageTitle] do gestor de fragmentos. Já não precisamos dele:

    // optional - gives a title to managed fragments
    @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 esta guia exibe? É importante compreender que guias e fragmentos são dois conceitos distintos. O fragmento exibido é sempre aquele escolhido pelo contêiner de fragmentos. Se alternar entre guias e não solicitar ao contêiner que altere o fragmento exibido, nada acontece: o mesmo fragmento continua a ser exibido, mas a guia selecionada mudou. Portanto, aqui, o fragmento exibido é aquele escolhido linha 17: o fragmento [Vue1Fragment];
  • linhas 26–30: o método a escrever para lidar com a mudança de separador do utilizador;

O método [onTabSelected] nas linhas 26–30 é acionado sempre que há uma mudança de separador (se o utilizador clicar num separador que já está selecionado, nada acontece). O seu código é o seguinte:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        if (IS_DEBUG_ENABLED) {
          Log.d("onglets", "onTabSelected");
        }
        // a tab has been selected - change the fragment displayed by the fragment container
        // miter position
        int position = tab.getPosition();
        // fragment number to display
        int numFragment;
        switch (position) {
          case 0:
            // fragment no. [Vue1Fragment]
            numFragment = FRAGMENTS_COUNT - 1;
            break;
          default:
            // fragment no. [PlaceholderFragment]
            numFragment = session.getNumFragment();
        }
        // fragment display
        mViewPager.setCurrentItem(numFragment);
}
  • linha 8: recuperamos a posição da guia que foi clicada. Aqui, obteremos um número 0 ou 1;
  • linhas 12–15: se a primeira guia foi clicada, preparamo-nos para exibir o fragmento [Vue1Fragment];
  • linhas 16–18: nos outros casos (se a guia n.º 2 foi clicada), preparamo-nos para voltar a exibir o fragmento que foi exibido da última vez que a guia n.º 2 foi selecionada. O seu ID foi então armazenado na sessão da aplicação;
  • linha 21: solicitamos ao contêiner de fragmentos que exiba o fragmento desejado;

Agora, vamos ver como gerir as opções do menu (ainda em [MainActivity]):


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // processing menu options
    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 processed
    return true;
}
  • linhas 16–31: tratamento das 4 opções do menu. Cada manipulador chama o método [showFragment] com o número do fragmento a ser exibido;

O método [showFragment] é o seguinte:


  // tab n° 2
  private TabLayout.Tab tab2 = null;
 
  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // if the 2nd tab doesn't yet exist, we create it
      if (tab2 == null) {
        tab2 = tabLayout.newTab();
        tabLayout.addTab(tab2);
      }
      // set the title of the second tab
      tab2.setText(String.format("Fragment n° %s", (i + 1)));
      // change the displayed fragment
      mViewPager.setCurrentItem(i);
      // the fragment number displayed is set to session
      session.setNumFragment(i);
      // tab 2 is selected - does nothing if already selected
      tab2.select();
    }
}
  • Lembre-se de que, quando a aplicação é iniciada, existe apenas um separador;
  • linha 2: uma referência à guia n.º 2, inicialmente nula;
  • linha 5: as condições de exibição não mudaram 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: o número do fragmento a ser exibido é colocado no título da segunda guia, com a numeração a começar em 1;
  • linha 14: o fragmento pretendido é exibido;
  • linha 16: o seu número é armazenado na sessão;
  • linha 18: a guia 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 instrui então o contêiner de fragmentos a exibir o fragmento já exibido na linha 14. Uma simples verificação no método [onTabSelected] evita este cenário:

        // display fragment only if necessary
        if (numFragment != mViewPager.getCurrentItem()) {
          mViewPager.setCurrentItem(numFragment);
}

Convidamos os leitores a testarem esta nova versão.

1.12.6. Melhorias

Agora temos uma compreensão sólida dos fragmentos, do seu ciclo de vida, do conceito de adjacência de fragmentos e da sua relação com a barra de separadores. Temos também uma arquitetura robusta que acabou de passar no teste do Exemplo 11:

  • uma atividade e n fragmentos;
  • todos os fragmentos estendem a classe [AbstractFragment];
  • os dados a serem partilhados entre fragmentos e entre fragmentos e a atividade são colocados na classe [Session];

Num novo projeto, iremos esclarecer as relações entre a atividade e os fragmentos adicionando uma interface.

1.13. Exemplo 12: Definir as relações entre a atividade e os fragmentos

Neste exemplo, queremos definir as relações mínimas entre a atividade e os fragmentos. Para tal, utilizaremos:

  • uma interface [IMainActivity] que define o que os fragmentos podem solicitar à atividade;
  • uma classe abstrata [AbstractFragment] que definirá o estado e os métodos que cada fragmento deve ter;

1.13.1. Criação do projeto

Duplicamos o projeto [Exemplo-11] para [Exemplo-12] seguindo o procedimento da secção 1.4. Obtemos o seguinte resultado:

1.13.2. A interface [IMainActivity]

A partir dos exemplos anteriores, fica claro que os fragmentos precisam de acesso à sessão instanciada pela atividade. Além disso, embora não seja visível nestes exemplos, é de esperar que os manipuladores de eventos nos fragmentos, por vezes, resultem numa alteração da vista. A atividade será solicitada a realizar esta alteração. A interface [IMainActivity] poderia então ter o seguinte aspeto:

  

package exemples.android;
 
public interface IMainActivity {
 
  // session access
  Session getSession();
 
  // change of view
  void navigateToView(int position);
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
}

Linha 12: Repare na presença de uma constante que anteriormente se encontrava na classe [MainActivity]. Queremos reduzir o acoplamento entre os fragmentos e a atividade e limitá-lo a um acoplamento entre [AbstractFragment] e [IMainActivity]. A atividade pode então ser nomeada de outra forma que não [MainActivity]. Uma vez que a constante [IS_DEBUG_ENABLED] é utilizada nos fragmentos, esta é movida para a interface [IMainActivity].

1.13.3. A classe abstrata [AbstractFragment]

A classe abstrata [AbstractFragment] sofre poucas alterações:


  // data accessible to daughter classes
  protected boolean afterViewsDone = false;
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
 
  // activity
  protected IMainActivity mainActivity;
  protected Activity activity;
 
...
  // update fragment
  protected void update() {
    // recover activity and session
    if (mainActivity == null) {
      this.activity = getActivity();
      if (this.activity != null) {
        this.mainActivity = (IMainActivity) activity;
        this.session = this.mainActivity.getSession();
      }
    }
    // the daughter class is asked to update itself
    updateFragment();
}
  • Linhas 6 e 7: mantemos dois tipos de referências à atividade:
    • linha 6: uma referência à atividade que implementa a interface [IMainActivity];
    • linha 7: uma referência à atividade que herda da classe [Activity] do Android. Este é o caso para todas as atividades;

Estas duas referências apontam naturalmente para o mesmo objeto. No entanto, este objeto é visto como dois tipos diferentes. Isto impedirá a conversão de tipos em tempo de execução;

  • linha 14: recuperamos uma referência à atividade utilizando o método [getActivity];
  • linha 15: se esta referência não for nula, então podemos aceder à sessão;
  • Linhas 16–17: armazenamos a atividade como uma implementação da interface [IMainActivity] e a sessão;

1.13.4. Modificar o gestor de fragmentos

O adaptador de fragmentos [SectionsPagerAdapter] na classe [MainActivity] é modificado num único local: em vez de gerir fragmentos do tipo [Fragment], agora gere fragmentos do tipo [AbstractFragment]:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private AbstractFragment[] fragments;
    // fragment no
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
      // initialization of fragment table
      fragments = new AbstractFragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        ...
      }
      // a fragment of +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }
 
    // fragment n° position
    @Override
    public AbstractFragment getItem(int position) {
      ...
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      ...
    }
}

1.13.5. Modificar a classe [MainActivity]

A classe [MainActivity] deve implementar a interface [IMainActivity]:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{
 
...
  // injection session
  @Bean(Session.class)
  protected Session session;
...
  // getter session
  public Session getSession() {
    return session;
  }
 
  @Override
  public void navigateToView(int position) {
    // the position view is displayed
    if(mViewPager.getCurrentItem()!=position){
      // fragment display
      mViewPager.setCurrentItem(position);
    }
  }
 
  • linhas 10–12: o método [getSession] já existia;
  • linhas 15–22: o método [navigateToView] exibe o fragmento #[position];
  • linha 17: verificamos se há algo a fazer;
  • linha 19: o fragmento #[position] é exibido;

Nesta altura, execute a aplicação. Deverá funcionar.

1.13.6. Modificar a exibição de fragmentos em [MainActivity]

Atualmente, a classe [MainActivity] exibe um fragmento utilizando a instrução:


    // view1 display
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

Uma vez que o método [navigateToView] faz o mesmo, substitua este tipo de instrução em todos os locais (2 locais) por:

navigateToView(...);

Em seguida, execute a aplicação. Deverá 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 exige que implementem o método [updateFragment]. Estes também devem ter um método [@AfterViews] no qual definem o booleano [afterViewsDone] como true;
  • uma sessão que encapsula os dados a serem partilhados entre os fragmentos e a atividade;

1.14. Exemplo-13: Exemplo-05 com fragmentos

No projeto [Exemplo-05], introduzimos a navegação entre vistas. Na altura, isso envolvia a navegação entre atividades: 1 vista = 1 atividade. Aqui, propomos ter uma única atividade com várias vistas do tipo [AbstractFragment].

1.14.1. Criação do projeto

Duplicamos o projeto anterior [Exemplo-12] para [Exemplo-13], seguindo o procedimento da secção 1.4. Obtemos o seguinte resultado:

1.14.2. Estrutura do projeto

Começaremos a utilizar pacotes para organizar o código. Por enquanto, podemos distinguir dois domínios distintos:

  • gestão de atividades;
  • gestão de fragmentos;

Criamos dois pacotes para eles: [examples.android.activity] e [examples.android.fragments]:

 

Fazemos o mesmo para criar o pacote [examples.android.fragments]:

Em [8], criamos um terceiro pacote chamado [architecture], no qual colocaremos as entidades [IMainActivity, AbstractFragment, Session, MyPager], que são os blocos de construção da arquitetura da nossa aplicação. Isto serve como um lembrete de que fizemos uma escolha arquitetónica específica. Em seguida, mova os elementos existentes do projeto conforme mostrado em [9]. Cada movimento deve ser confirmado clicando no botão [Refactor].

Nesta altura, compile a aplicação. Temos os seguintes erros em [MainActivity]:

 

Ao mover classes para pacotes, o Android Studio efetuou as alterações necessárias no código da aplicação (linhas 18–21, por exemplo). As classes referenciadas nas linhas 15 e 17 não foram movidas. Estas são geradas pela biblioteca Android Annotations. Para estas classes, deve alterar as importações manualmente. Estas linhas passam, portanto, a ser:

 

Depois de fazer isto, já não haverá erros de compilação. Execute a aplicação. Verá 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 especificam que a atividade designada é [examples.android.MainActivity_]. No entanto, uma vez que a atividade foi movida para o pacote [activity], a linha 12 deve agora ser:


      android:name=".activity.MainActivity_"

Repare no ponto (.) antes de [activity]. Mais uma vez, o Android Studio não conseguiu atualizar o manifesto porque este faz referência a uma classe do Android Annotations que não foi movida. A utilização da biblioteca AA acarreta, portanto, uma série de inconvenientes.

1.14.3. Limpar o projeto

No novo projeto:

  • já não existem separadores, botões flutuantes ou menus;
  • os fragmentos [PlaceholderFragment] desaparecem. A aplicação irá gerir dois fragmentos: [Vue1Fragment], que já temos, e [Vue2Fragment], que teremos de criar;
  • a sessão já não é a mesma;

1.14.3.1. Limpar os fragmentos

Elimine a classe [PlaceHolderFragment] [1]:

 

Da mesma forma, elimine a vista [res/layout/fragment_main.xml] associada a este fragmento [2].

1.14.3.2. Limpar a sessão

A sessão encontra-se atualmente da seguinte forma:


package exemples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
  // n° fragment type [PlaceholderFragment] displayed in second tab
  private int numFragment;
 
  // getters and 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 estamos a guardar nada desta sessão.

Compile o projeto. As linhas que causam erros são aquelas que utilizavam o conteúdo da sessão. Remova-as. Na classe [Vue1Fragment], removemos também a variável [numVisit] do código, que fica da seguinte forma:


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 {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}

1.14.3.3. Remover as guias, o botão flutuante e o menu

A remoção das guias 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];

O menu também é removido 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>
  • Remova as linhas [28-31, 41-47];
  • remova 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>
  • Vamos remover as linhas 9–24. Isto deixa uma opção que não iremos utilizar. Apenas para fornecer um exemplo de uma declaração de opção de menu que pode ser replicada através de copiar/colar;

Na classe [MainActivity], remova tudo o que se refira aos separadores, ao botão flutuante, à barra de ferramentas e ao menu. A forma mais fácil de encontrar estas referências é remover as suas declarações:


  // the tab manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // the floating button
  @ViewById(R.id.fab)
protected FloatingActionButton fab;

e recompile a aplicação. As linhas com erros são aquelas que fazem referência aos elementos em falta. Por isso, elimine todas essas linhas. Além disso, modifique o gestor de fragmentos para que este deixe de fazer referência ao fragmento [PlaceholderFragment] que eliminámos:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private AbstractFragment[] fragments;
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
    }
 
    // fragment n° position
    @Override
    public AbstractFragment getItem(int position) {
      // log
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      return fragments.length;
    }
}
  • Linhas 7–10: Removemos toda a geração de fragmentos;

Nesta altura, já não deverá 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 {
 
  // the fragment container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
// the toolbar
@ViewById(R.id.toolbar)
protected Toolbar toolbar;
 
  // injection session
  @Bean(Session.class)
  protected Session session;
 
  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
  private final int OFF_SCREEN_PAGE_LIMIT = 2;
 
  // debug mode
  public static final boolean IS_DEBUG_ENABLED = true;
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // manufacturer
  public MainActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "constructor");
    }
  }
 
  @AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
 
    // toolbar - this is where the application name is displayed
    setSupportActionBar(toolbar);
 
    // the fragment manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
 
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i issued by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // fragment offset
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
 
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
 
    // no scrolling
    mViewPager.setScrollingEnabled(false);
 
    // view1 display
    navigateToView(FRAGMENTS_COUNT - 1);
 
  }
 
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
  }
 
  // getter session
  public Session getSession() {
    return session;
  }
 
  @Override
  public void navigateToView(int position) {
    // the position view is displayed
    if (mViewPager.getCurrentItem() != position) {
      // fragment display
      mViewPager.setCurrentItem(position);
    }
  }
 
  // the fragment manager
  // it is used to request fragments to be displayed in the main view
  // must define methods [getItem] and [getCount] - the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private AbstractFragment[] fragments;
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
    }
 
    // fragment n° position
    @Override
    public AbstractFragment getItem(int position) {
      // log
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      return fragments.length;
    }
  }
}

Há mais algumas alterações a fazer:

  • apague a linha 31, que já não é necessária;
  • linha 33: defina a adjacência do fragmento como 1;
  • linha 76: navegue para a vista 0. Esta será a primeira a ser exibida;
  • linha 108: inicialize a matriz com o fragmento [Vue1Fragment_]:

    // fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};

Portanto, temos apenas um fragmento. Execute a aplicação. Deverá obter o seguinte resultado:

Image

O botão [Validate] deve funcionar.

1.14.4. Criação de fragmentos e vistas associadas

A aplicação terá duas vistas, as do projeto [Example-05]. Já temos a vista [vue1.xml] no projeto atual. Vamos agora duplicar [vue2.xml] do [Example-05] para o [Example-12] (abra ambos os projetos e copie e cole entre eles).

 
  • Em [1], a nova vista. Quando tentamos editá-la, surgem erros [2]. Precisamos de modificar o ficheiro [strings.xml] [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>
  <!-- vue 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <!-- vue 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 [View1Fragment] para [View2Fragment]:

  

e modifique 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() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}
  • linha 9: o fragmento está associado à vista [res/layout/view2.xml];
  • linha 10: a classe estende a classe abstrata [AbstractFragment];
  • linhas 12–20: o método obrigatório [@AfterViews];
  • linhas 23–25: o método obrigatório [updateFragment];

1.14.5. Implementação de fragmentos e navegação entre eles

A atividade irá agora gerir dois fragmentos. A sua classe [SectionsPagerAdapter] é atualizada da seguinte forma:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
 
    ...
}

A interface [IMainActivity] gere a navegação entre vistas utilizando o seu método [navigateToView]. Iremos tratar do clique no botão [View 2] no 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 {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // event managers ----------------------------------
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
  @Click(R.id.buttonVue2)
  protected void showVue2() {
    mainActivity.navigateToView(1);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}
  • linhas 37–40: o método [showVue2] trata do evento «click» no botão [View #2];
  • linha 39: a navegação é realizada utilizando o método [navigateToView] da atividade. Recorde-se que a atividade foi armazenada na classe pai como:

  // activity
protected IMainActivity mainActivity;

e que esta atividade já tenha sido inicializada ao entrar em qualquer manipulador de eventos.

  • linha 34: a instrução utiliza a variável [activity] da classe pai, que é uma referência à atividade como uma instância do tipo [Activity] do Android;

protected Activity activity;

Encontramos 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() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // gestionnaires d'évts ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}
  • Linhas 24–27: O método [showVue1] trata do evento «click» no botão [View 1];

Execute o projeto e verifique se a navegação entre as vistas funciona.

1.14.6. Definir a sessão

A aplicação funciona da seguinte forma:

  • Introduza um nome na Vista 1;
  • Exiba esse nome na Vista 2;

Para permitir que a Vista 1 passe o nome introduzido para a Vista 2, utilizaremos a seguinte sessão;


package exemples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // name
  private String nom;
 
  // getters and setters
...
}
  • linha 8: o nome introduzido;

A classe [MainActivity] irá inicializar a sessão da seguinte forma:


  // injection session
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // init session
    session.setNom("");
}

1.14.7. Código final para os fragmentos

No fragmento [Vue1Fragment], modificamos o código do manipulador de clique do botão [Validate]:


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 {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
...
  // event managers ----------------------------------
 
  @Click(R.id.buttonValider)
  protected void doValider() {
    // memorizes the name entered
    String nom = editTextNom.getText().toString();
    // we display it
    Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
  }
 
  @Click(R.id.buttonVue2)
  protected void showVue2() {
    // enter the name entered in the session
    session.setNom(editTextNom.getText().toString());
    // navigate to view no. 2
    mainActivity.navigateToView(1);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
 
  }
}
  • linhas: 31-37: processam o clique no botão [View #2];
  • linha 34: antes de navegar para a Vista 2, guardamos o nome introduzido na sessão para que a nova vista possa aceder ao mesmo;

A vista [View2Fragment] 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 {
 
  // visual interface components
  @ViewById(R.id.textViewBonjour)
  protected TextView textViewBonjour;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // gestionnaires d'évts ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
    // retrieve the name entered in the session
    String nom = session.getNom();
    // we display it
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
  }
}

Quando a vista n.º 2 for apresentada, o nome introduzido na vista n.º 1 deve ser apresentado. Sabemos que, imediatamente após a sua apresentação, o método [updateFragment] será executado. É, portanto, neste método (linhas 36–42) que podemos colocar 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 1 é recuperado da sessão;
  • Linha 41: O rótulo [textViewBonjour] é atualizado;

Execute o projeto e verifique se funciona.

1.14.8. Gerir o ciclo de vida do fragmento

No fragmento [Vue1Fragment], o método [@AfterViews] é o seguinte:


  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
}

Este método está incompleto. Na verdade, temos de ter sempre em conta o caso em que o fragmento é reciclado após uma operação [onDestroyView]. Neste caso, a vista do Fragmento 1 é regenerada e qualquer nome que possa ter sido introduzido anteriormente desaparecerá da vista. Não queremos isso. Atualmente, o nome inserido permanece visível porque a adjacência dos fragmentos do Fragmento 1 significa que o ciclo de vida do fragmento [Vue1Fragment] é executado apenas uma vez. No entanto, é preferível ter em conta a reciclagem do fragmento.

Existem várias formas de resolver este problema:

  • podemos tirar partido do facto de o método [update] ser executado sistematicamente sempre que o fragmento é apresentado para atualizar o nome introduzido;
  • pode realizar esta atualização apenas quando o método [@AfterViews] for reexecutado. Vamos adotar a última abordagem;

Modificamos o código em [View1Fragment] da seguinte forma:


    // visual interface elements
    @ViewById(R.id.editTextNom)
    protected EditText editTextNom;
 
    // data
    private String nom;
 
    @AfterViews
    protected void afterViews() {
        // memory
        afterViewsDone = true;
        // log
        if (isDebugEnabled) {
            Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
        }
        // the text displayed is (re)initialized
        editTextNom.setText(nom);
    }
 
    // event managers ----------------------------------
 
...
 
    @Click(R.id.buttonVue2)
    protected void showVue2() {
        // the name entered is noted so that it can be retrieved if the fragment is recycled
        nom = editTextNom.getText().toString();
        // enter the name entered in the session
        session.setNom(nom);
        // navigate to view no. 2
        activity.navigateToView(1);
}
  • linha 27: como estamos prestes a sair da vista 1 para a vista 2, guardamos o nome introduzido;
  • linha 17: cada vez que o ciclo de vida do fragmento é executado, o último nome introduzido é apresentado novamente;

Para o fragmento [View2Fragment], o código existente é suficiente:


  // visual interface components
  @ViewById(R.id.textViewBonjour)
  protected TextView textViewBonjour;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
    // retrieve the name entered in the session
    String nom = session.getNom();
    // we display it
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
  • O único componente visual da vista (linha 3) é atualizado sempre que a vista é exibida (linha 21). O método [@AfterViews] não tem, portanto, nada a acrescentar;

1.14.9. Conclusão

Neste ponto, demonstrámos mais uma vez a relevância da nossa arquitetura:

  • uma atividade que implementa a interface [IMainActivity];
  • fragmentos que estendem a classe [AbstractFragment], o que exige que implementem o método [updateFragment]. Estes também devem ter um método [@AfterViews] no qual definem o booleano [afterViewsDone] como verdadeiro;
  • uma sessão que encapsula os dados a serem partilhados entre os fragmentos e a atividade;

1.15. Exemplo-14: Uma arquitetura de duas camadas

Vamos construir uma aplicação de vista única com a seguinte arquitetura:

1.15.1. Criação do projeto

Duplicamos o projeto anterior [Exemplo-12] para [Exemplo-13], seguindo o procedimento descrito na secção 1.4. Obtemos o seguinte resultado:

1.15.2. A vista [view1]

A aplicação terá apenas uma vista [view1.xml]. Por conseguinte, iremos eliminar a outra vista [view2.xml] juntamente com o seu fragmento associado:

 

Compile a aplicação. Aparecem erros em [MainActivity]:

 

Corrija a linha 4 abaixo no gestor de fragmentos [SectionsPagerAdapter]


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...

A linha 4 acima passa a ser:


    // fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};

Remova as importações que já não são necessárias [Ctrl-Shift-O]. Já não deverá haver erros de compilação. Execute o projeto: a vista n.º 1 deverá aparecer. Vamos agora modificá-la.

Vamos criar a vista [vue1.xml] que irá gerar números aleatórios:

 

Os seus componentes são os seguintes:

N.º
ID
Tipo
Função
1
edtNbAleas
EditText
número de números aleatórios a gerar no intervalo inteiro [a,b]
2
edtA
EditText
valor de um
2
edtB
EditText
valor de b
4
btnExecute
Botão
inicia a geração de números
5
ListView
lstRespostas
lista de números gerados na ordem inversa à da geração. O número gerado mais recentemente é apresentado em primeiro lugar;

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>
  <!-- vue 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 em [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>
  <!-- colors applied -->
  <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

  

Uma vez que aqui existe apenas um fragmento, não é necessário planear a comunicação entre fragmentos. A sessão será, portanto, vazia:


package exemples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
}

Neste ponto, compile a aplicação. Serão apresentados erros nas linhas que utilizaram elementos da sessão, agora vazia. Remova essas linhas e verifique se a compilação já não produz erros.

1.15.4. O fragmento [Vue1Fragment]

  

Modificamos 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 {
 
  // visual interface elements
  @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;
 
  // list of order responses
  private List<String> reponses = new ArrayList<>();
  // listview adapter
  private ArrayAdapter<String> adapterReponses;
 
  // seizures
  private int nbAleas;
  private int a;
  private int b;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
    // hide error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
  }
 
  @Click(R.id.btn_Executer)
  void doExecuter() {
    // hide any previous error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
  }
 
  // check the validity of the data entered
  private boolean isPageValid() {
...
  }
 
  @Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
  }
}
  • Existe aqui apenas um fragmento cujo ciclo de vida será executado apenas uma vez, no arranque da aplicação. Por este motivo, o método [@AfterViews] (linhas 46–57) e o método [updateFragment] (linhas 75–81) serão executados apenas uma vez no arranque da aplicação;
  • linhas 55-56: ocultamos as duas mensagens de erro da vista (mostradas abaixo) [1-2];
 
  • linhas 59-60: o método executado quando o botão [Executar] é clicado;
  • linhas 71-73: verifica-se a validade das entradas;

O método [isPageValid] é o seguinte:


  // seizures
  private int nbAleas;
  private int a;
  private int b;
 
...
 
// check the validity of the data entered
  private boolean isPageValid() {
    // enter the number of random numbers
    nbAleas = 0;
    Boolean erreur;
    int nbErreurs = 0;
    try {
      nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
      erreur = (nbAleas < 1);
    } catch (Exception ex) {
      erreur = true;
    }
    // mistake?
    if (erreur) {
      nbErreurs++;
      txtErrorAleas.setVisibility(View.VISIBLE);
    }
    // enter a
    a = 0;
    erreur = false;
    try {
      a = Integer.parseInt(edtA.getText().toString());
    } catch (Exception ex) {
      erreur = true;
    }
    // mistake?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // b input
    b = 0;
    erreur = false;
    try {
      b = Integer.parseInt(edtB.getText().toString());
      erreur = b < a;
    } catch (Exception ex) {
      erreur = true;
    }
    // mistake?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // return
    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 alguma entrada for inválida, as mensagens de erro associadas são exibidas;

Nesta altura, a aplicação está executável. Verifique a funcionalidade do método [isPageValid] introduzindo dados incorretos.

1.15.5. A camada [business]

  

A camada [de negócios] fornece a seguinte interface [IMetier]:


package exemples.android.metier;
 
import java.util.List;
 
public interface IMetier {
 
    List<Object> getAleas(int a, int b, int n);
}

O método [getAleas(a,b,n)] normalmente devolve n números inteiros aleatórios no intervalo [a,b]. Também foi concebido para lançar uma exceção uma em cada três vezes, e essa exceção é incluída nos resultados devolvidos pelo método. Em última análise, o método 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) {
        // object list
        List<Object> réponses = new ArrayList<Object>();
        // some checks
        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"));
        }
        // mistake?
        if (réponses.size() != 0) {
            return réponses;
        }
        // random numbers are generated
        Random random = new Random();
        for (int i = 0; i < n; i++) {
            // generate a random exception 1 time / 3
            int nombre = random.nextInt(3);
            if (nombre == 0) {
                réponses.add(new AleaException("Exception aléatoire"));
            } else {
                // otherwise a random number is returned between two bounds [a,b]
                réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
            }
        }
        // result
        return réponses;
    }
}
  • Linha 9: Utilizamos a anotação AA [@EBean] na classe [Business] para que possamos injetar referências a ela na camada [Presentation]. O atributo (scope = EBean.Scope.Singleton) garante que apenas uma única instância da classe [Business] será criada. Portanto, a mesma referência é sempre injetada, mesmo que seja injetada várias vezes na camada [Presentation];
  • o resto do código é padrão;

O tipo [AleaException] utilizado pela classe [Metier] é o seguinte:


package exemples.android.metier;
 
public class AleaException extends RuntimeException {
 
    private static final long serialVersionUID = 1L;
 
    public AleaException() {
    }
 
    public AleaException(String detailMessage) {
        super(detailMessage);
    }
 
    public AleaException(Throwable throwable) {
        super(throwable);
    }
 
    public AleaException(String detailMessage, Throwable throwable) {
        super(detailMessage, throwable);
    }
 
}
  • Linha 3: A classe [AleaException] estende a classe do sistema [RuntimeException], tornando-a uma exceção não tratada: não precisa de ser tratada num bloco try/catch, nem precisa de ser incluída nas assinaturas dos métodos;

1.15.6. A [MainActivity] revisitada

  

[Negócio] CamadaAtividadeUtilizadorVista

A atividade implementará a interface [IMetier] da camada [business]. Assim, o fragmento/visualização terá apenas a atividade como sua contraparte.

A atividade [MainActivity] já implementa a interface [IMainActivity]. Para que ela também implemente a interface [IMetier], podemos:

  • adicionar a interface [IMetier] às interfaces implementadas pela atividade;
  • garantir que a própria interface [IMainActivity] estenda a interface [IMetier]. Esta é a abordagem que estamos a adotar;

A interface [IMainActivity] passa a ter a seguinte forma:

  

package exemples.android.architecture;
 
import exemples.android.metier.IMetier;
 
public interface IMainActivity extends IMetier {
 
  // session access
  Session getSession();
 
  // change of view
  void navigateToView(int position);
 
  // debug mode
  public static final boolean IS_DEBUG_ENABLED = true;
 
}
  • linha 5: a interface [IMainActivity] estende a interface [IMetier]

A classe [MainActivity] evolui da seguinte forma:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
 
  ...
 
  // injection session
  @Bean(Session.class)
  protected Session session;
 
  // injection molding
  @Bean(Metier.class)
  protected IMetier metier;
 
...
  // implémentation IMetier --------------------------------------------------------------------
  @Override
  public List<Object> getAleas(int a, int b, int n) {
    return metier.getAleas(a, b, n);
}
  • linhas 11-12: a camada [business] é injetada na atividade. Para isso, usamos a anotação [@Bean], cujo parâmetro é a classe que possui a anotação [@EBean];
  • linha 2: a atividade implementa a interface [IMainActivity] e, por conseguinte, a interface [IMetier] da camada [business];
  • linhas 16–19: implementação do único método da interface [IMetier]. Simplesmente delegamos a chamada à camada [business];

1.15.7. O fragmento [Vue1Fragment] revisitado

  

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 {
 
  // visual interface elements
  @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;
 
  // list of order responses
  private List<String> reponses = new ArrayList<>();
  // listview adapter
  private ArrayAdapter<String> adapterReponses;
 
  // seizures
  private int nbAleas;
  private int a;
  private int b;
 
  @AfterViews
  protected void afterViews() {
   ...
  }
 
  @Click(R.id.btn_Executer)
  void doExecuter() {
  ...
  }
 
  // check the validity of the data entered
  private boolean isPageValid() {
   ...
  }
 
  @Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
    // will only be executed once when the application is started
    // create the ListView adapter - this requires the [activity] variable to have been initialized
    adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
  }
}
  • Linhas 69-70: Define o adaptador para o componente [ListView];

O componente [ListView] é utilizado para apresentar uma lista de itens. Faz isso utilizando um adaptador [ListAdapter], que por sua vez está ligado à fonte de dados que alimenta o [ListView]. Para definir o adaptador para um [ListView], utilize o seguinte método [ListView.setAdapter]:


public void setAdapter (ListAdapter adapter)

[ListAdapter] é uma interface. A classe [ArrayAdapter] é uma classe que implementa esta interface. O construtor utilizado na linha 69 acima é o seguinte:


public ArrayAdapter (Context context, int resource, int textViewResourceId, List<T> objects)
  • [context] é a atividade que exibe a [ListView];
  • [resource] é o número inteiro que identifica a vista utilizada para apresentar um item na [ListView]. Esta vista pode ter qualquer grau de complexidade. O programador constrói-a de acordo com as suas necessidades;
  • [textViewResourceId] é o número inteiro que identifica um componente [TextView] na vista [resource]. A cadeia de caracteres exibida será mostrada por este componente;
  • [objects]: a lista de objetos exibidos pela [ListView]. O método [toString] dos objetos é utilizado para exibir o objeto na [TextView] identificada por [textViewResourceId] dentro da vista identificada por [resource].

A tarefa do programador é criar a vista [resource] que irá exibir cada item na [ListView]. Para o caso simples em que queremos apenas exibir uma única sequência 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 [ListView]. Este adaptador só precisa de ser definido uma vez. Para permitir a sua reutilização, foi definido como uma 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 de um fragmento através de [getActivity] e armazenada aqui na variável [activity] da classe pai. Este campo nem sempre tem um valor. Assim, os registos mostram que, quando chegamos ao método [@AfterViews], ele ainda não foi inicializado, pelo que não podemos colocar as linhas 69–70 neste método. No método [updateFragment], isto é possível porque sabemos que, quando este método é executado, [activity] não é necessariamente nulo. O adaptador está aqui associado à fonte de dados [reponses] definida na linha 37;

O método [doExecute] trata do clique no botão [Execute]. O seu código é o seguinte:


@Click(R.id.btn_Executer)
  void doExecuter() {
    // hide any previous error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // delete previous answers
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
    // we ask for the random numbers in the activity
    List<Object> data = mainActivity.getAleas(a, b, nbAleas);
    // we create a list of Strings from this data
    for (Object o : data) {
      if (o instanceof Exception) {
        reponses.add(((Exception) o).getMessage());
      } else {
        reponses.add(o.toString());
      }
    }
    // refresh listview
    adapterReponses.notifyDataSetChanged();
  }
  • linhas 7-8: queremos limpar a ListView. Para isso, limpamos a fonte de dados [reponses] e solicitamos ao adaptador associado à ListView que atualize;
  • linhas 10-12: Antes de executar a ação solicitada, verificamos se os valores introduzidos estão corretos;
  • linha 14: a lista de números aleatórios é solicitada à atividade. Obtemos uma lista de objetos em que cada objeto é do tipo [Integer] ou [AleaException];
  • linhas 16–22: utilizando a lista de objetos obtida, a fonte de dados [reponses] apresentada pelo ListView é atualizada;
  • linha 24: o adaptador ListView é solicitado a atualizar-se;

1.15.8. Execução

Execute o projeto e verifique se funciona corretamente.

1.16. Exemplo-15: Arquitetura cliente/servidor

Vamos agora analisar uma arquitetura comum para uma aplicação Android, na qual a aplicação Android comunica com serviços web remotos. Teremos agora a seguinte arquitetura:

Adicionámos uma camada [DAO] à aplicação Android para comunicar com o servidor remoto. Esta 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 URLs específicas na 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 irá devolver um número aleatório no intervalo [a,b]. Iremos descrever a aplicação pela seguinte ordem:

O servidor

  • a sua camada [business];
  • o seu serviço [web/JSON] implementado com Spring MVC;

O cliente

  • a sua camada [DAO]. Não haverá camada [de negócios];

1.16.1. O servidor [web/JSON]

Queremos construir a seguinte arquitetura:

1.16.1.1. Criação do projeto

Iremos construir o serviço web utilizando 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 o qual o Android Studio não oferece assistência:

  • em [1]: escolha um projeto Gradle;
  • em [2-3]: as propriedades da dependência JAR gerada pelo projeto (ver abaixo);
  • em [4]: selecione a dependência web [5] para que os binários necessários para o nosso serviço web fiquem disponíveis;
  • em [6]: gere o projeto. É então gerado um ficheiro ZIP de um projeto Gradle de base, que fica disponível para download;

O que deve colocar em [2-3]? Já utilizámos dependências Gradle. Por exemplo, a do projeto anterior era a seguinte:

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}
 
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
 
android {
  compileSdkVersion 23
  ...
}
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 é especificada no formato [groupId:artifactId:version]. O que é solicitado no formulário em [http://start.spring.io/]:
    • em [2] é [groupId];
    • em [3] é [artifactId];

Descompacte o ficheiro zip obtido na pasta que contém os outros projetos:

Utilizando o Android Studio, abra o projeto Gradle [server-01] [1-2]. O projeto aberto encontra-se em [3] (vista Projeto).

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 IDE Eclipse. Removemo-las;
  • As linhas 1–11 e 15 são usadas para adicionar um plugin chamado [spring-boot] ao nosso projeto Gradle. O Spring Boot é um projeto dentro do ecossistema Spring [http://projects.spring.io/spring-boot/]. Este plugin define as versões das dependências mais comumente usadas com o Spring. Isto permite-nos omitir a especificação das suas versões (linhas 30 e 31). A versão é então aquela definida pela versão do Spring Boot utilizada (linha 3);
  • linhas 22–23: a versão Java a utilizar, neste caso a versão 1.8;
  • linhas 25–27: os repositórios binários a utilizar para descarregar dependências;
  • linha 26: especifica o Repositório Central do Maven. Este é atualmente o maior repositório binário 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 testes, particularmente testes JUnit;
  • Uma dependência [compile] indica que a dependência é necessária para compilar o projeto. Uma dependência [testCompile] indica que a dependência é necessária apenas para executar testes. Por conseguinte, não é incluída no binário do projeto;

Vamos começar por limpar o 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'
 
// binaire du projet
jar {
  baseName = 'server-01'
  version = '0.0.1-SNAPSHOT'
}
 
// versions Java
sourceCompatibility = 1.8
targetCompatibility = 1.8
 
// dépôts Maven
repositories {
  mavenLocal()
  mavenCentral()
}
 
// dépendances
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 para a máquina de desenvolvimento. Este é criado quando o Maven é instalado (ver secção 6.10). Se a dependência solicitada já estiver no repositório Maven local, não será obtida do repositório Maven central;
  • linhas 19–22: uma tarefa Gradle para gerar o binário do projeto. Iremos utilizá-la para ver o que está a ser feito;
  • Em [1-4], execute a tarefa [jar] definida no ficheiro [build.gradle] ([1] está localizado no canto superior direito e na lateral do IDE);

O passo anterior cria o arquivo JAR do projeto e coloca-o na pasta [build/libs] [5]:

  

O nome do arquivo é derivado diretamente das informações fornecidas à tarefa [jar] no ficheiro [build.gradle] (linhas 19–22).

Todas as dependências do projeto podem ser visualizadas da seguinte forma:

 

Podemos ver em [1] que a única dependência do projeto [compile('org.springframework.boot:spring-boot-starter-web')] trouxe consigo dezenas de binários. O Spring Boot para 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:

  • inclui as dependências de que provavelmente precisaremos;
  • inclui um servidor Tomcat incorporado [1], o que nos poupa de ter de implementar a aplicação num servidor web externo;

Pode encontrar muitos exemplos que utilizam o Spring Boot no site do ecossistema Spring [http://spring.io/guides].

Vamos agora completar o ficheiro [build.gradle] da seguinte forma:


// spring boot
...
// dépendances
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}
 
// plugin pour créer un binaire aux normes Maven dans le dépôt 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 {
      // change to point to your repo, e.g. http://my.org/repo
      url 'file://D:\\maven'
    }
  }
}
  • linha 10: importamos um plugin Gradle chamado [maven-publish] que nos permite publicar o binário do projeto num repositório Maven, de acordo com os padrões do Maven;
  • linha 11: uma tarefa Gradle chamada [publishing];
  • linhas 14–15: as características do binário Maven que 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, em [2], executarmos a tarefa [publish], o binário do projeto é criado e instalado na pasta especificada na linha 23 do ficheiro [build.gradle]:

 

A tarefa [jar] gera o binário do projeto. Este binário não inclui as suas dependências e, por isso, não é executável. É possível gerar um binário executável que inclua todas as suas dependências. Para tal, adicionamos o seguinte código ao ficheiro [build.gradle]:


// créer un binaire avec toutes ses dépendances
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: Introduza o nome completo da classe executável do projeto:
  

O código para esta aula 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 precisamos de configurar o projeto. Uma vez que este não é um projeto Android gerado pelo IDE, esta configuração — que ainda não realizámos até agora — deve ser feita aqui.

 
  • Em [3-4]: utilize o JDK 1.8;

Para compilar o projeto, o botão disponível para projetos Android já não está presente. Vamos utilizar uma opção do menu [1-2]:

Em seguida, o leitor é solicitado a criar o seguinte projeto. Iremos comentar o código final do projeto [3].

1.16.1.4. A camada [business]

  

A camada [de negócios] segue a mesma abordagem que a camada [de negócios] do exemplo anterior. Terá a seguinte interface [IMetier]:


package exemples.android.server.metier;
 
public interface IMetier {
  // random number in [a,b]
    int getAlea(int a, int b);
}
  • Linha 5: o método que gera 1 número aleatório no intervalo [a,b]

O código para a 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) {
    // some checks
    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);
    }
    // result generation
    Random random=new Random();
    random.setSeed(new Date().getTime());
    return a + random.nextInt(b - a + 1);
  }
}

Não faremos comentários sobre a classe: ela é semelhante à do exemplo anterior, exceto pelo facto de não lançar exceções aleatoriamente. Observe a anotação Spring [@Service] na linha 8, que faz com que o Spring instancie a classe como uma instância única (singleton) e disponibilize a sua referência para outros componentes Spring. Outras anotações Spring poderiam ter sido usadas aqui para alcançar o mesmo efeito. Os componentes Spring têm nomes predefinidos que podem ser especificados como um atributo da anotação utilizada. Sem este atributo, como aqui, o componente Spring assume o nome da classe com o primeiro caractere em minúscula. Assim, aqui, o componente Spring é denominado [metier] por predefinição;

A classe [Metier] lança exceções do tipo [AleaException]:


package exemples.android.server.metier;
 
public class AleaException extends RuntimeException {
 
  // error code
  private int code;
 
  // manufacturers
  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 and setters
....
}
  • linha 3: [AleaException] estende a classe [RuntimeException]. Trata-se, portanto, de uma exceção não tratada (não é necessário tratá-la com um try/catch);
  • linha 6: é adicionado 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 padrão arquitetónico MVC (Modelo–Visão–Controlador) da seguinte forma:

O processamento de um pedido do cliente decorre da seguinte forma:

  1. solicitação - os URLs solicitados têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... O [Dispatcher Servlet] é a classe Spring que lida com os URLs recebidos. Ele «encaminha» o URL para a ação que deve tratá-lo. Estas ações são métodos de classes específicas chamadas [Controllers]. O C em MVC refere-se aqui à cadeia [Dispatcher Servlet, Controller, Action]. Se nenhuma ação tiver sido configurada para tratar a URL recebida, o [Dispatcher Servlet] responderá que a URL solicitada não foi encontrada (erro 404 NOT FOUND);
  1. o processamento
  • a ação selecionada pode utilizar os parâmetros que o [Servlet Dispatcher] lhe passou. Estes podem provir de várias fontes:
    • o caminho [/param1/param2/...] da URL,
    • os parâmetros da URL [p1=v1&p2=v2],
    • dos parâmetros enviados pelo navegador com o seu pedido;
  • Ao processar o pedido do utilizador, a ação poderá necessitar da camada [de negócios] [2b]. Depois de o pedido do cliente ter sido processado, poderá desencadear várias respostas. Um exemplo clássico é:
    • uma página de erro, caso a solicitação não tenha sido processada corretamente
    • uma página de confirmação, caso contrário
  • a ação instrui que uma vista específica seja exibida [3]. Esta vista exibirá dados conhecidos como o modelo de vista. Este é o M em MVC. A ação criará este modelo M [2c] e instruirá que uma vista V seja exibida [3];
  1. resposta - a vista V selecionada utiliza o modelo M construído 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 modificada:

  • em [4a], o modelo, que é uma classe Java, é convertido 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 numa cadeia de caracteres JSON e de deserialização de uma cadeia de caracteres JSON num objeto Java é apresentado nos apêndices da Secção 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 [Resposta] da seguinte forma:


package exemples.android.server.web;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}
  • linha 13: o campo [T body] é a resposta esperada pelo cliente. Decidimos usar aqui uma resposta genérica do tipo T, em vez do tipo Integer do número aleatório esperado. Queremos poder reutilizar esta classe noutras situações. Ao processar o pedido do cliente, o servidor pode encontrar 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 status != 0, uma lista de mensagens de erro — normalmente as da pilha de exceções, caso tenha ocorrido uma exceção — nulo 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 {
 
  // business layer
  @Autowired
  private IMetier metier;
  // mapper JSON
  @Autowired
  private ObjectMapper mapper;
 
  // random numbers
  @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 {
 
    // the answer
    Response<Integer> response = new Response<>();
    // we use the business layer
    try {
      response.setBody(metier.getAlea(a, b));
      response.setStatus(0);
    } catch (AleaException e) {
      response.setStatus(e.getCode());
      response.setMessages(getMessagesFromException(e));
    }
    // we return the answer
    return mapper.writeValueAsString(response);
  }
 
  private List<String> getMessagesFromException(Throwable e) {
    // message list
    List<String> messages = new ArrayList<String>();
    // browse the exception stack
    Throwable th = e;
    while (th != null) {
      messages.add(e.getMessage());
      th = th.getCause();
    }
    // we return the result
    return messages;
  }
 
}
  • linha 17: a anotação [@Controller] indica que a classe é um controlador MVC cujos métodos tratam de pedidos para determinados URLs na aplicação web;
  • linhas 21–22: a anotação [@Autowired] instrui o Spring a injetar um componente do tipo [IMetier] no campo. Esta será a classe [Metier] anterior. Como adicionámos a anotação [@Service] a ela, é tratada como um componente Spring;
  • linhas 24–25: fazemos o mesmo com um mapeador JSON que definiremos mais tarde. O nosso serviço web enviará a sua resposta como uma cadeia JSON. Este mapeador tratará da serialização da resposta para JSON;
  • linha 30: o método que gera o número aleatório. O seu nome não importa. Quando é executado, os seus parâmetros já 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 na linha 28;
  • linha 28: a anotação [@RequestMapping] define certas propriedades do método anotado:
    • [value]: a URL aceite pelo método;
    • [method]: o método HTTP aceite pelo método. Existem principalmente dois: GET e POST. O método [POST] é utilizado quando o cliente pretende anexar um documento à sua solicitação HTTP;
    • [produces]: define um dos cabeçalhos da resposta HTTP que será enviada ao cliente. Aqui, entre os cabeçalhos HTTP enviados com a resposta ao cliente, haverá um que informa ao cliente que a resposta está a ser enviada na forma de uma cadeia JSON. Este cabeçalho não é obrigatório. É fornecido ao cliente para fins informativos, caso o cliente espere respostas que possam assumir várias formas;
    • [consumes]: não está presente aqui. Especifica os cabeçalhos HTTP que devem acompanhar a solicitação HTTP do cliente para que esta seja aceita;
  • 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 é tratada como uma chave utilizada para selecionar a página HTML a enviar ao cliente. Num serviço web / JSON, não existem páginas HTML;
  • linha 28: o URL processado tem o formato /{a}/{b}, onde {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 uma URL e, portanto, são do tipo String. A conversão de String para o tipo do parâmetro pode falhar. O Spring MVC lança então uma exceção. Resumindo: se eu solicitar a URL /100/200 num navegador, o método getAlea na linha 30 será executado com os parâmetros inteiros a=100, b=200;
  • linha 36: é solicitado à camada [business] um número aleatório no intervalo [a,b]. Recorde-se que o método [business].getAlea pode lançar uma exceção;
  • linha 37: sem erro;
  • linha 39: código de erro;
  • linha 40: a lista de mensagens de resposta é a da pilha de exceções (linhas 46–57). Aqui, sabemos que a pilha contém apenas uma exceção, mas queríamos demonstrar um método mais genérico;
  • linha 43: a resposta do tipo [Response<Integer>] é devolvida como uma cadeia JSON;

1.16.1.6. Configuração do projeto Spring

  

Existem várias formas de configurar o Spring:

  • usando ficheiros XML;
  • com código Java;
  • utilizando uma combinação de ambos;

Optamos por configurar a nossa aplicação web utilizando código Java. A seguinte classe [Config] trata desta 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 {
  // web configuration ------------------------------------
  @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);
  }
 
  // mapper jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
 
}
  • Linha 12: Indicamos ao Spring em que pacotes encontrará os dois componentes que precisa de gerir:
    • o componente [Metier] anotado com [@Service] no pacote [exemples.android.server.metier];
    • o componente [WebController] anotado com [@Controller] no pacote [examples.android.server.web];
  • linha 13: a anotação [@EnableWebMvc] permite que o Spring Boot lide automaticamente com várias configurações padrão para uma aplicação Spring MVC. Isto reduz significativamente a carga de trabalho do programador;
  • linhas 16, 22, 27 e 33: a anotação [@Bean] também define componentes Spring (beans) da mesma forma que as duas anotações encontradas (@Service, @Controller). Aqui, a anotação [@Bean] anota um método em vez de uma classe, e o resultado do método é o componente Spring. Na ausência de um atributo de nomeação dentro da anotação [@Bean], o componente Spring criado assume o nome do método anotado;
  • linhas 16–20: definem o bean [dispatcherServlet]. Este é um nome predefinido no Spring MVC que define o controlador frontal da aplicação MVC, um objeto pelo qual todas as solicitações do cliente passam e que as despacha (daí o seu nome) para os vários [@Controller]s na 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] é utilizado para definir quais as URLs aceites pela aplicação. Na linha 24, todas as URLs são aceites;
  • linhas 27–30: o bean [embeddedServletContainerFactory] é utilizado para definir o servidor incorporado nas dependências do projeto que irá hospedar a aplicação web. A linha 29 especifica que este é um servidor Tomcat e que será executado na porta 8080. Por predefinição, os binários para este servidor web são fornecidos pela dependência [org.springframework.boot:spring-boot-starter-web] no ficheiro Gradle;

1.16.1.7. Executar o 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) {
    // application execution
    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. Aqui, trata-se da classe [Config] que acabámos de descrever. O segundo parâmetro é a matriz de argumentos passados ao método [main] (linha 7);

A aplicação web pode ser iniciada de várias formas, incluindo as seguintes:

 

Em seguida, aparecem várias mensagens de registo na consola:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.1.RELEASE)

2014-10-07 09:13:42.194  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Starting Application on Gportpers3 with PID 7408 (D:\data\istia-1415\android\dvp\exemples\exemple-10-server\target\classes started by ST in D:\data\istia-1415\android\dvp\exemples)
2014-10-07 09:13:42.294  INFO 7408 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@23edbbb9: startup date [Tue Oct 07 09:13:42 CEST 2014]; root of context hierarchy
2014-10-07 09:13:42.996  INFO 7408 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-10-07 09:13:44.251  INFO 7408 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-10-07 09:13:44.665  INFO 7408 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-10-07 09:13:44.666  INFO 7408 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.54
2014-10-07 09:13:44.866  INFO 7408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-10-07 09:13:44.866  INFO 7408 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2575 ms
2014-10-07 09:13:45.748  INFO 7408 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-10-07 09:13:45.750  INFO 7408 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-10-07 09:13:46.802  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.946  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/{a}/{b}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public android.exemples.server.web.AleaResponse android.exemples.server.web.AleaController.getAlea(int,int)
2014-10-07 09:13:46.950  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-10-07 09:13:46.951  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:47.294  INFO 7408 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-10-07 09:13:47.335  INFO 7408 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-10-07 09:13:47.337  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Started Application in 6.081 seconds (JVM running for 6.897)
  • linhas 12-14: o servidor incorporado do Tomcat é iniciado;
  • linhas 15-19: o servlet Spring MVC [DispatcherServlet] é carregado e configurado;
  • linha 20: a URL do servidor web [/{a}/{b}] é detetada;

Agora, vamos abrir um navegador e testar a URL JSON do serviço web:

De cada vez, obtemos 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] para o navegador Chrome (ver apêndices, secção 6.13):

Image

  • em [1], o URL solicitado;
  • em [2], utilizando um pedido GET;
  • em [3], a solicitação é enviada;

Image

  • em [4], os cabeçalhos HTTP da resposta do servidor. Note-se que isto indica que o documento enviado é uma cadeia JSON;
  • em [5], a string JSON recebida;

1.16.1.8. Gerando o JAR executável do projeto

Na secção 1.16.1.2, mostrámos como configurar o ficheiro Gradle para gerar um executável para a aplicação com todas as suas dependências. Adaptada à aplicação atual, esta configuração passa a ser a seguinte:


// créer un binaire avec toutes ses dépendances
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, siga estes passos [1-5]:

Para o executar, pare o serviço web se estiver em execução [1] e, em seguida, execute o arquivo [2-4]:

 

Abra um navegador e aceda ao URL [localhost:8080/100/200]. Deverá obter os mesmos resultados que anteriormente.

1.16.1.9. Gestão de registos

Ao executar o arquivo executável, você notará que os logs são diferentes dos que aparecem quando executa o projeto a partir do IDE. Você verá logs 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)

Pode gerir o nível de registo adicionando um ficheiro [logback.xml] à pasta [resources] do projeto:

  

Este ficheiro pode ter o seguinte conteúdo:


<configuration>
 
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoders are  by default assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
 
  <!-- log level control -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

O nível de registo é controlado na linha 12. Se agora recompilarmos o arquivo executável e o executarmos, obtemos 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 para o servidor web / JSON

O cliente Android terá a seguinte arquitetura:

O cliente terá dois componentes:

  1. uma camada [Apresentação] (view+activity) semelhante à que estudámos no Exemplo [Exemplo-14];
  2. a camada [DAO] que interage com o serviço [web / JSON] que estudámos anteriormente.

1.16.2.1. Criação do projeto

Duplicamos o projeto anterior [Exemplo-14] no [Exemplo-15], seguindo o procedimento descrito na secção 1.4. Obtemos o seguinte resultado:

A seguir, convidamos o leitor a criar o projeto a seguir.

1.16.2.2. Configuração do Gradle

 

O ficheiro [build.gradle] é o seguinte:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}
 
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
 
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "exemples.android"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
 
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
 
  // options de packaging nécessaires pour être capable de produire l'APK
  packagingOptions {
    exclude 'META-INF/ASL2.0'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/notice.txt'
    exclude 'META-INF/license.txt'
  }
}
 
def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  apt "org.androidannotations:rest-spring:$AAVersion"
  compile "org.androidannotations:rest-spring-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
  compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
  compile fileTree(include: ['*.jar'], dir: 'libs')
  testCompile 'junit:junit:4.12'
}
 
repositories {
  maven {
    url 'https://repo.spring.io/libs-milestone'
  }
}

Iremos comentar apenas o que ainda não foi abordado:

  • linhas 46–47: inserção de um plugin AA. O plugin [rest-spring-api] permite que a comunicação cliente/servidor seja delegada à biblioteca AA;
  • linha 50: a biblioteca [spring-android-rest-template] é a biblioteca utilizada pelo AA para gerir a comunicação 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 conseguinte, devemos 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, ocorrem erros ao gerar o binário APK do projeto;

1.16.2.3. O manifesto da aplicação Android

  

O ficheiro [AndroidManifest.xml] precisa de ser atualizado. Por predefinição, o acesso à Internet está desativado. Deve ser ativado utilizando 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: O acesso à Internet é permitido;

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 {
 
  // random number
  int getAlea(int a, int b);
 
  // URL of the web service
  void setUrlServiceWebJson(String url);
 
  // max wait time (ms) for server response
  void setTimeout(int timeout);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
}
  • linha 6: o serviço web / método JSON para obter um número aleatório no intervalo [a,b] a partir deste serviço web;
  • linha 9: a URL do serviço web / JSON para gerar números aleatórios;
  • linha 12: definimos um tempo limite máximo para aguardar a resposta do servidor;
  • linha 15: queremos definir um tempo limite antes de executar o pedido ao servidor, para dar tempo ao utilizador para cancelar o seu pedido;

1.16.2.4.2. A interface [WebClient]
  

A interface [WebClient] gere a comunicação 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 random number in the range [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 utilizando as anotações que iremos adicionar-lhe. Esta interface deve implementar chamadas para os URLs expostos pelo serviço web / JSON:

  // random number
  @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] garante que, quando o servidor envia uma cadeia JSON, esta é automaticamente deserializada. Assim, vemos na linha (d) que a URL [/{a}/{b}] retorna um tipo String, que é, na verdade, uma string JSON (linha b). Com esta informação e a do tipo esperado na linha 16, a instância [WebClient] do cliente irá deserializar a string que recebe num tipo [Response<Integer>];
  • linha 15: uma anotação @Get que indica que a URL deve ser chamada utilizando o método HTTP GET. O parâmetro da anotação @Get é o formato de URL esperado pelo serviço web. Basta utilizar o parâmetro [value] da anotação @RequestMapping (linha b) do método chamado no [WebController] do servidor. As chaves {} delimitam os parâmetros da URL que devem ser passados para os parâmetros do método na linha 16. A sintaxe [@Path("a") int a] faz com que ao parâmetro [a] do método seja atribuído o valor {a} da URL. Quando o parâmetro da URL e o parâmetro do método têm o mesmo nome, como neste caso, podemos escrever de forma mais simples [@Path int a];

No caso de um pedido 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] designa o valor enviado. Este será automaticamente serializado para JSON. No lado do servidor, teremos a seguinte assinatura:


  // random numbers
  @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 que é esperada uma solicitação HTTP POST e que o corpo da solicitação (objeto enviado) deve ser transmitido como uma string JSON (atributo consumes);
  • linha 4: o valor enviado será recuperado no parâmetro [@RequestBody T body] do método;

Voltemos ao código da classe [WebClient]:


@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
  • Precisamos de poder especificar o URL do serviço web a contactar. Isto é conseguido através da extensão da interface [RestClientRootUrl] fornecida pelo AA. Esta interface expõe um método [setRootUrl(urlServiceWeb)] que nos permite definir o URL do serviço web a contactar;
  • Além disso, queremos controlar a chamada ao serviço web porque pretendemos limitar o tempo de espera pela resposta. Para tal, estendemos a interface [RestClientSupport], que expõe o método [setRestTemplate] que nos permitirá:
    • criar nós próprios o objeto [RestTemplate], que é utilizado para gerir as trocas cliente/servidor;
    • configurar este objeto para definir o tempo máximo de espera da 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> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}

Esta é a classe [Response] já utilizada no lado do servidor (secção 1.16.1.5). De facto, do ponto de vista da programação, é como se a camada [DAO] do cliente estivesse a comunicar diretamente com o [WebController] do serviço web:

A comunicação de rede entre o cliente e o servidor, bem como a serialização/desserialização de objetos Java no lado do cliente, são transparentes para o programador.

1.16.2.4.4. Implementação da camada [DAO]
  

A interface [IDao] é implementada com a seguinte classe [Dao]:


package exemples.android.dao;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
 
import java.util.ArrayList;
import java.util.List;
 
@EBean
public class Dao implements IDao {
 
  // service customer REST
  @RestService
  protected WebClient webClient;
 
  // mapper jSON
  private ObjectMapper mapper = new ObjectMapper();
  // timeout before request execution
  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 possamos injetar noutro local;
  • linhas 19–20: injetamos a implementação da interface [WebClient] que descrevemos. A anotação [@RestService] trata desta injeção;
  • os outros métodos implementam a interface [IDao] (linhas 27–46);

método [setTimeout]

O método [setTimeout] é o seguinte:


  @Override
  public void setTimeout(int timeout) {
    // set the client request timeout REST
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
    // we build the restTemplate
    RestTemplate restTemplate = new RestTemplate(factory);
    // set the jSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // set the restTemplate of the web client
    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 utilizando uma classe [RestTemplate];
  • linha 4: a classe [SimpleClientHttpRequestFactory] é fornecida pela dependência [spring-android-rest-template]. Ela permitirá definir o tempo máximo de espera para a resposta do servidor (linhas 5-6);
  • linha 8: construímos o objeto [RestTemplate], que servirá como canal de comunicação com o serviço web. Passamos o objeto [factory] que acabámos de construir como parâmetro para ele;
  • linha 10: o diálogo cliente/servidor pode assumir várias formas. As trocas ocorrem através de linhas de texto, e temos de indicar ao objeto [RestTemplate] o que fazer com cada linha de texto. Para tal, fornecemos-lhe conversores — classes capazes de processar linhas de texto. A escolha do conversor é geralmente feita através dos cabeçalhos HTTP que acompanham a linha de texto. Aqui, sabemos que estamos a receber apenas linhas de texto no formato JSON. Além disso, como vimos na secção 1.16.1.7, o servidor enviou o cabeçalho HTTP:

Content-Type: application/json;charset=UTF-8 

Linha 10: o único conversor para o [RestTemplate] será um conversor JSON implementado utilizando a biblioteca [Jackson]. Há uma particularidade relativa a estes conversores: o AA exige que o incluamos também na anotação [WebClient]:


@Rest(converters = {MappingJacksonHttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

Linha 1: É necessário especificar um conversor, mesmo que já o estejamos a especificar programaticamente.

  • Linha 12: O objeto [RestTemplate] construído desta forma é injetado na implementação da interface [WebClient], e é este objeto que irá tratar da comunicação cliente/servidor;

Método [getAlea]

O método [getAlea] é o seguinte:


  @Override
  public int getAlea(int a, int b) {
    // service execution
    Response<Integer> info;
    DaoException ex;
    try {
      // waiting
      waitSomeTime(delay);
      // service execution
      info = webClient.getAlea(a, b);
      int status = info.getStatus();
      if (status == 0) {
        // we return the result
        return info.getBody();
      } else {
        // we note the exception
        ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
      }
    } catch (JsonProcessingException | RuntimeException e) {
      // we note the exception
      ex = new DaoException(e, 100);
    }
    // we launch the exception
    throw ex;
  }
...
  // private methods -------------------
  private void waitSomeTime(int delay) {
    try {
      Thread.sleep(delay);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
}
  • linha 8: esperar [delay] milissegundos;
  • linha 10: chamamos simplesmente o método com a mesma assinatura na classe que implementa a interface [WebClient];
  • linha 11: analisamos a resposta recebida do servidor verificando o seu [status];
  • linhas 12–14: se não houve erro do lado do servidor (status = 0), então devolvemos o resultado do método;
  • linha 17: se houve um erro do lado do servidor (status!=0), então preparamos uma exceção sem a lançar. O servidor enviou uma lista de mensagens de erro. Criamos uma exceção com, como única mensagem, a string JSON da lista de mensagens do servidor;
  • linhas 19–22: outros casos de exceção;
  • linha 24: quando chegamos a este ponto, ocorreu necessariamente uma exceção. Por isso, lançamo-la;

A [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 {
 
  // error code
  private int code;
 
  // manufacturers
  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 and setters
...
}
  • linha 6: a [DaoException] é uma exceção não tratada;

Método [setUrlServiceWebJson]

O método [setUrlServiceWebJson] é o seguinte:


  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // we set the URL of the REST service
    webClient.setRootUrl(urlServiceWebJson);
}
  • Linha 4: Definimos a URL do serviço web utilizando o método [setRootUrl] da interface [WebClient]. Este método existe porque esta interface estende a interface [RestClientRootUrl];

1.16.2.5. O pacote [architecture]

O pacote [architecture] contém os elementos que estruturam a aplicação:

1.16.2.5.1. A interface [IMainActivity]

A interface [IMainActivity] lista os métodos que a atividade da aplicação deve implementar:


package exemples.android.architecture;
 
import exemples.android.dao.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  Session getSession();
 
  // change of view
  void navigateToView(int position);
 
  // waiting
  void beginWaiting();
 
  void cancelWaiting();
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
  // response time
  int TIMEOUT = 1000;
  // fragment adjacency
  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 o ecrã de carregamento da aplicação (linhas 14, 16);
  • linha 21: definimos um tempo limite máximo para a resposta do servidor de 1 segundo;

1.16.2.5.2. A classe [Utils]

Agrupámos métodos utilitários estáticos na classe [Utils] que podem ser chamados a partir de várias partes da arquitetura da aplicação:


package exemples.android.architecture;
 
import java.util.ArrayList;
import java.util.List;
 
public class Utils {
 
  // list of exception messages - version 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // create a list of error msgs from the exception stack
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }
 
  // exception message list - version 2
  static public String getMessagesForAlert(Throwable th) {
    // build the text to be displayed
    StringBuilder texte = new StringBuilder();
    List<String> messages = getMessagesFromException(th);
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // result
    return texte.toString();
  }
 
}
  • linhas 9–18: cria uma lista de mensagens de erro contidas num Throwable;
  • linhas 21-32: utiliza o método anterior para construir, a partir da lista de mensagens obtida, o texto a ser exibido 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 dois objetivos:

  1. garantir que o método [updateFragments] das classes filhas seja sempre chamado quando o fragmento é exibido, e apenas uma vez;
  2. separar o estado e os métodos das classes filhas que podem ser separados;

É o objetivo 2 que nos leva a incluir operações de gestão de imagens de espera nesta classe: todos os componentes de uma aplicação Android assíncrona devem lidar com este tipo de problema:


  // wait management
  protected void beginWaiting() {
    // we set the hourglass
    mainActivity.beginWaiting();
  }
 
  protected void cancelWaiting() {
    // the hourglass is removed
    mainActivity.cancelWaiting();
}

1.16.2.6. A vista

1.16.2.6.1. A vista [view1.xml]
  

Em comparação com o exemplo anterior, a vista [view1.xml] sofreu as seguintes alterações:

 
 
  • em [1], o utilizador deve especificar o URL do serviço web e o tempo limite [2] antes de cada chamada ao serviço web;
  • em [3], as respostas são contadas;
  • em [4], o utilizador pode cancelar o seu pedido;
  • em [5], aparece um indicador de carregamento quando os números são solicitados. Este desaparece assim que todos os números tiverem sido recebidos ou a operação tiver sido cancelada;

Image

  • Em [6], verifica-se a validade das entradas;

Convidamos o leitor a carregar o ficheiro [vue1.xml] dos exemplos. Para o resto desta secção, fornecemos os IDs dos novos componentes:

Image

N.º
Tipo
ID
1
EditText
edt_nbaleas
2
TextView
txt_errorNumberOfEpisodes
3
EditText
edt_a
4
EditText
edt_b
5
TextView
txt_errorInterval
6
EditText
editTextWebServiceUrl
7
TextView
textViewErrorUrl
8
EditText
editTextDelay
9
TextView
textViewErrorDelay
10
Botão
btn_Executar
11
Botão
btn_Cancelar
12
TextView
txt_Respostas
13
ListView
lst_answers

Os botões [10-11] estão fisicamente uns sobre os outros. Em qualquer momento, apenas um dos dois estará 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 {
 
  // visual interface elements
  @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;
...
  // local data
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;
 
  @AfterViews
  void afterViews() {
    // memory
    afterViewsDone=true;
    // initially no error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    textViewErreurDelay.setVisibility(View.INVISIBLE);
    // hidden [Cancel] button
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);
    // list of answers
    reponses = new ArrayList<>();
  }
...
  • linhas 24–49: referências aos componentes da vista [view1.xml] (linha 20);
  • linhas 55-69: o método [@AfterViews] executado quando as referências nas linhas 24-49 tiverem sido inicializadas;
  • linha 58: não se esqueça disto — necessário para o ciclo de vida do fragmento;
  • linhas 60–63: as mensagens de erro são ocultadas;
  • linhas 65–66: o botão [Cancel] é ocultado (linha 65) e o botão [Execute] é exibido (linha 66). Note que eles estão fisicamente um sobre o outro;
  • Linha 68: O campo na linha 52 conterá a lista de cadeias de caracteres a serem exibidas pelo ListView de respostas;

Imediatamente após o método [@AfterViews], será executado o seguinte método [updateFragment]:


  @Override
  protected void updateFragment() {
    // create the response list adapter
    adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
}
  • Linhas 4-5: Crie o adaptador ListView para as respostas. Este é armazenado numa variável de instância para que esteja disponível para outros métodos da classe;

Clicar no botão [Execute] aciona a execução do seguinte método:


// seizures
  private int nbAleas;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int delay;
 
  // local data
  private int nbInfos;
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;
  private boolean hasBeenCanceled;
 
  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // delete previous answers
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // reset the response counter to 0
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
    // activity initialization
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // we ask for the random numbers
    for (int i = 0; i < nbAleas; i++) {
      getAlea(a, b);
    }
    // we start waiting
    beginWaiting();
  }
 
  @Background(id = "alea")
  void getAlea(int a, int b) {
    // do as little as possible here
    // in any case no display - these must take place in the UiThead
    try {
      // the result is displayed in the UiThread
      showInfo(mainActivity.getAlea(a, b));
    } catch (RuntimeException e) {
      // the exception is displayed in the UiThread
      showAlert(e);
    }
  }
  • linhas 17–18: limpamos a lista anterior de respostas do servidor. Para isso, na linha 17, limpamos a fonte de dados [reponses] associada ao adaptador ListView;
  • linha 19: um valor booleano que nos dirá se o utilizador cancelou ou não o seu pedido;
  • linhas 21-22: exibimos um contador definido como zero para o número de respostas;
  • linhas 24–26: recuperamos as entradas das linhas [2–6] e verificamos a sua validade. Se alguma delas for inválida, o método é abortado (linha 25) e o utilizador é redirecionado para a interface visual;
  • linhas 28-29: se todos os dados introduzidos forem válidos, então o URL do serviço web (linha 28) e o tempo de espera antes de cada chamada de serviço (linha 29) são passados para a atividade. Esta informação é exigida pela camada [DAO], e note-se que é a atividade que comunica com ela;
  • linhas 31–33: os números aleatórios são solicitados um a um ao método [getAlea] na linha 39;
  • linha 38: o método [getAlea] é anotado com a anotação AA [@Background], o que significa que será executado numa thread (fluxo de execução, processo) diferente daquela em que a interface visual é executada. É, de facto, obrigatório executar qualquer chamada à Internet numa thread diferente daquela da interface visual. Assim, em qualquer momento, podem existir várias threads:
    • aquela que exibe a IU (Interface do Utilizador) e gere os seus eventos,
    • as threads [nbAleas], cada uma das quais solicita um número aleatório ao serviço web. Estas threads são lançadas de forma assíncrona: a thread da IU lança uma thread [getAlea] (linha 32) que solicita um número aleatório ao serviço web e não espera que este termine. Será notificada da conclusão através de um evento. Assim, as threads [nbAleas] serão lançadas em paralelo. É possível configurar a aplicação para que lance apenas uma thread de cada vez. Nesse caso, existe uma fila de threads a serem executadas;

Linha 38: o parâmetro [id] atribui um nome à thread gerada. Aqui, todas as threads [nbAleas] têm o mesmo nome [alea]. Isto permitirá cancelá-las todas ao mesmo tempo. Este parâmetro é opcional se o cancelamento de threads não for tratado;

  • Linha 44: O método [getAlea] da atividade é chamado. Será, portanto, executado numa thread separada da interface do utilizador. Esta thread fará a chamada ao serviço web e não aguardará a resposta. Será notificada posteriormente, através de um evento, de que a resposta está disponível. Neste ponto, 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 lançar uma exceção. Solicitamos então que as mensagens de erro da exceção sejam apresentadas numa mensagem de alerta;
  • Linha 35: Aguardamos os resultados:
    • será exibido um indicador de carregamento;
    • o botão [Cancelar] substituirá o botão [Executar]. Como as threads iniciadas são assíncronas, a thread da interface do utilizador não aguarda por elas, e a linha 35 é executada antes de elas terminarem. Assim que o método [beginWaiting] terminar, a interface do utilizador pode voltar a responder ao utilizador, por exemplo, a um clique no botão [Cancelar]. Se as threads iniciadas fossem síncronas, a linha 35 só seria alcançada depois de todas as threads terem terminado. Cancelá-las deixaria então de fazer sentido;

O método [showInfo] é o seguinte:


  @UiThread
  protected void showInfo(int alea) {
    if (!hasBeenCanceled) {
      // one more piece of information
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // are we done?
      if (nbInfos == nbAleas) {
        // we end the wait
        cancelWaiting();
      }
      // we add the information to the list of answers
      reponses.add(0, String.valueOf(alea));
      // we display the answers
      adapterReponses.notifyDataSetChanged();
    }
}
  • O método [showInfo] é chamado dentro da thread [getAlea] anotada com [@Background]. Este método irá atualizar a interface do utilizador. Só o pode fazer executando-se dentro da thread da interface do utilizador. 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 é executado apenas se o utilizador não tiver cancelado o seu pedido;
  • linhas 5–6: o contador de respostas é incrementado e exibido;
  • linhas 8–11: se todas as respostas esperadas tiverem sido recebidas, a espera é encerrada (fim do sinal de espera; o botão [Execute] substitui o botão [Cancel]);
  • linhas 12–15: o número aleatório recebido é adicionado à lista de respostas exibida pelo componente [ListView listReponses] e a lista é atualizada;

O método [showAlert] é o seguinte:


  @UiThread
  protected void showAlert(Throwable th) {
    if (!hasBeenCanceled) {
      // we cancel everything
      doAnnuler();
      // we display it
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}

A lógica aqui é semelhante à do método [showInfo]:

  • linha 1: a anotação [@UiThread] é obrigatória;
  • linha 2: o método recebe a exceção que ocorreu;
  • linha 3: o método é executado apenas se o utilizador não tiver cancelado o seu pedido;
  • linha 5: a solicitação do utilizador é cancelada como se ele próprio tivesse clicado no botão [Cancel];
  • linha 7: o alerta é exibido utilizando a classe [AlertDialog] do Android:
    • [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 que a janela de alerta seja exibida;
 

Ao clicar no botão [Cancelar], o seguinte método é executado:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // memory
    hasBeenCanceled=true;
    // the asynchronous task is cancelled
    BackgroundExecutor.cancelAll("alea", true);
    // end of wait
    cancelWaiting();
}
  • linha 4: note que o utilizador cancelou o seu pedido;
  • linha 6: cancela todas as tarefas identificadas pela string [alea]. O segundo parâmetro [true] significa que elas 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] estava a comportar-se incorretamente. Foi por isso que adicionámos o booleano [hasBeenCanceled]. De facto, no caso de uma exceção (servidor em baixo), a janela de alerta apareceria 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 carregamento à vista associada à [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">
      <!-- image d'waiting -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>
 
    </android.support.v7.widget.Toolbar>
    <!-- image d'waiting -->
  </android.support.design.widget.AppBarLayout>
...
  • linhas 17-21: a imagem de espaço reservado;

1.16.2.7.2. A atividade [MainActivity]

A [MainActivity] mudou pouco em relação ao que era no [Exemplo-14]. Primeiro, injetamos a camada [DAO] nela:


  // dao injection
  @Bean(Dao.class)
  protected IDao dao;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // set the [DAO] layer
    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: definir o tempo limite para a camada [DAO]

Além disso, a atividade [MainActivity] deve implementar a interface [IMainActivity], que por sua vez estende a interface [IDao]:


  // implémentation IMainActivity --------------------------------------------------------------------
  @Override
  public void navigateToView(int position) {
    // the position view is displayed
    if (mViewPager.getCurrentItem() != position) {
      // fragment display
      mViewPager.setCurrentItem(position);
    }
  }
 
  // hold image management
  public void cancelWaiting() {
    loadingPanel.setVisibility(View.INVISIBLE);
  }
 
  public void beginWaiting() {
    loadingPanel.setVisibility(View.VISIBLE);
  }
 
  // implémentation IDao --------------------------------------------------------------------
 
  @Override
  public int getAlea(int a, int b) {
    // execution
    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. Executar o projeto

Inicie o serviço web (secção 1.16.1.7) e, em seguida, inicie o cliente Android:

Image

Para saber o que introduzir em [1], siga estes passos. Abra um prompt 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 convenientes porque não são bloqueados pela firewall do Windows. A linha 30 mostra o endereço IP do seu computador numa rede local. Para utilizar este endereço, geralmente é necessário desativar a firewall do Windows. Se estiver ligado a uma rede Wi-Fi, utilize o endereço Wi-Fi e, também neste caso, desative a firewall, se tiver uma.

Teste a aplicação nos seguintes casos:

  • 100 números aleatórios no intervalo [1000, 2000] sem limite de tempo;
  • 2000 números aleatórios no intervalo [10000, 20000] sem limite de tempo e cancele a espera antes de a geração terminar;
  • 5 números aleatórios no intervalo [100, 200] com um tempo de espera de 5000 ms, e cancele a espera antes de a geração terminar;

1.16.2.9. Tratamento do cancelamento

Para acompanhar o que acontece quando o utilizador solicita o cancelamento ou quando o cancelamento é desencadeado por uma exceção, adicionamos o seguinte método à interface [IDao] (ver secção 1.16.2.4.1):


package exemples.android.dao;
 
public interface IDao {
 
  ...
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
}

Na classe [Dao], adicionamos o seguinte código:


  // debug mode
  private boolean isDebugEnabled;
  // class name
  private String className;
..
  // manufacturer
  public Dao() {
    // class name
    className = getClass().getSimpleName();
  }
...
  // interface IDao -------------------------------------------------------------------
  @Override
  public int getAlea(int a, int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // service execution
    Response<Integer> info;
...
  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
}
  • linha 9: anotamos 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) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    ....
  }
 
  @UiThread
  protected void showAlert(Throwable th) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    ...
    }
}
 
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
   ...
}

Sempre que o fragmento [Vue1Fragment] recebe informações da camada [DAO], é gerado um registo. Além disso, quando o método [doAnnuler] é chamado, o evento é registado.

Teste 1

Solicitamos 5 números, apesar de o servidor não ter sido iniciado. Obtemos os seguintes registos:

06-06 08:48:51.571 15317-16201/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.576 15317-16202/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.585 15317-16204/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.586 15317-16203/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.593 15317-16205/exemples.android D/Dao_: getAlea [100, 200] en cours
...
06-06 08:48:53.568 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.568 15317-15317/exemples.android D/Vue1Fragment_: Annulation demandée
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
  • Linhas 1–5: O método [getAlea] da classe [Dao] é chamado cinco vezes. Note-se que estas são chamadas assíncronas feitas pelo fragmento [VueFragment] e que o fragmento não aguarda o resultado da sua chamada;
  • linha 7: a primeira solicitação HTTP foi feita e o fragmento [VueFragment] recebeu a sua primeira exceção;
  • linha 8: solicita então o cancelamento de todas as solicitações;
  • linhas 9–12: no entanto, vemos que recebe as quatro exceções seguintes. Portanto, as solicitações assíncronas pendentes foram todas executadas;

Teste 2

Agora, vamos iniciar o servidor e solicitar 5 números com um atraso de 5 segundos; em seguida, clicar em [Cancelar] antes do fim do atraso. Os registos são os seguintes:

06-06 09:12:38.360 4640-5054/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.360 4640-5055/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.361 4640-5056/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.362 4640-5057/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.363 4640-5058/exemples.android D/Dao_: getAlea [100, 200] en cours
...
06-06 09:12:39.895 4640-4640/exemples.android D/Vue1Fragment_: Annulation demandée
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:30:00.150 1616-1616/exemples.android D/Vue1Fragment_: showInfo(157)
06-06 09:30:00.151 1616-1616/exemples.android D/Vue1Fragment_: showInfo(157)
  • linhas 1-5: o método [getAlea] da classe [Dao] é chamado cinco vezes;
  • linha 7: o utilizador solicitou que os pedidos fossem cancelados;
  • linha 8: vemos que [Vue1_Fragment] recebe 5 valores. Mais uma vez, todas as solicitações assíncronas pendentes foram executadas;

É por isso que tivemos de gerir um booleano [hasBeenCanceled] para evitar a exibição de qualquer coisa quando um cancelamento tivesse sido solicitado. No código de cancelamento:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // memory
    hasBeenCanceled = true;
    // the asynchronous task is cancelled
    BackgroundExecutor.cancelAll("alea",true);
    // end of wait
    cancelWaiting();
}

O código na linha 10 não faz o que se espera. Isto pode dever-se ao facto de as tarefas assíncronas partilharem o mesmo método anotado com [@Background]:


  @Background(id = "alea")
  void getAlea(int a, int b) {
    ...
}

1.17. Exemplo-16: Gerir a assincronia com o RxAndroid

Vamos agora gerir a assincronia necessária para aplicações Android utilizando uma biblioteca chamada RxJava [http://reactivex.io/] e a sua versão derivada para o ambiente Android [RxAndroid]. Para tal, utilizaremos o curso [Introdução ao RxJava. Aplicação aos ambientes Swing e Android].

1.17.1. Criação do projeto

Duplicamos o projeto [Exemplo-1] para [Exemplo-16]:

1.17.2. Configuração do Gradle

  

No ficheiro [build.gradle], adicionamos a dependência da biblioteca [RxAndroid]:


dependencies {
  ...
  compile 'io.reactivex:rxandroid:1.2.0'
}

1.17.3. A camada [DAO]

  

1.17.4. A interface [IDao]

A interface [IDao] passa a ser a seguinte:


package exemples.android.dao;
 
import rx.Observable;
 
public interface IDao {
 
  // random number
  Observable<Integer> getAlea(int a, int b);
 
  // URL of the web service
  void setUrlServiceWebJson(String url);
 
  // max wait time (ms) for server response
  void setTimeout(int timeout);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
}
  • linha 8: o método [getAlea] agora retorna um tipo [Observable] da biblioteca RxJava (linha 3). O princípio é o seguinte:

Um fluxo de elementos do tipo Observable<T> é observado por um ou mais assinantes (observadores, consumidores) do tipo Subscriber<T>. A biblioteca RxJava permite que o fluxo Observable<T> seja executado na thread T1 e o seu observador Subscriber<T> na thread T2, sem que o programador tenha de se preocupar com a gestão do ciclo de vida destas threads e com questões naturalmente complexas, tais como a partilha de dados entre threads e a sincronização de threads para executar uma tarefa global. Facilita, assim, a programação assíncrona.

1.17.5. A classe [AbstractDao]

Iremos 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 {
 
  // mapper jSON
  private ObjectMapper mapper = new ObjectMapper();
 
  // méthodes protégées ----------------------------------------------------------
  // generic interface
  protected interface IRequest<T> {
    Response<T> getResponse();
  }
 
  // generic request
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // service execution
    return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // service execution
        try {
          // make the synchronous request and forward the response to the subscriber
          Response<T> response = request.getResponse();
          // mistake?
          int status = response.getStatus();
          if (status != 0) {
            // we note the exception
            ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
          } else {
            // we issue the answer
            subscriber.onNext(response.getBody());
            // we signal the end of the observable
            subscriber.onCompleted();
          }
        } catch (JsonProcessingException | RuntimeException e) {
          // we note the exception
          ex = new DaoException(e, 100);
        }
        // exception?
        if (ex != null) {
          // we issue the exception
          subscriber.onError(ex);
        }
      }
    });
  }
 
}
  • A classe [AbstractDao] tem como elemento principal um método genérico [getResponse] utilizado para recuperar um [Response<T>] do servidor, onde 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 retorna o [Response<T>] desejado;
  • Graças aos dois pontos anteriores, a classe [AbstractDao] pode servir como classe pai para qualquer camada [Dao] do lado do cliente de um servidor que envie respostas do tipo [Response<T>];
  • linha 20: o método genérico [getResponse] retorna um tipo [Observable<T>] que representa o resultado efetivamente esperado pelo cliente HTTP (aqui, 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 que um elemento do tipo T seja emitido para um observador;
    • [onError(Throwable th)]: permite que uma exceção seja emitida para um observador;
    • [onCompleted]: permite indicar a um observador que as emissões terminaram;

Um tipo [Observable<T>] obedece a certas restrições:

  • emite os seus elementos utilizando o método [onNext(T element)];
  • o método [onCompleted] deve ser chamado exatamente uma vez assim que não houver mais elementos para emitir ao observador;
  • o método [onCompleted] não é chamado se o método [onError(Throwable th)] tiver sido chamado;

No nosso exemplo:

  • o observador será o fragmento [Vue1Fragment]. É o observador que consome os elementos emitidos pelo [Observable<T>] (elemento ou exceção);
  • o tipo [Observable<T>] criado emitirá apenas um único elemento (linha 37);
  • linha 29: faz uma solicitação HTTP síncrona ao servidor e obtém o tipo [Response<T>]. Esta solicitação HTTP é tratada pelo tipo [IRequest] passado como parâmetro ao método genérico [getResponse];
  • linha 31: recupera o estado da resposta;
  • linhas 32–34: se este estado indicar um erro, é preparada uma exceção;
  • linhas 36–39: se o estado não for um erro, a resposta efetivamente esperada pelo cliente é enviada (linha 37) e o observador é notificado de que não haverá mais emissões (linha 39);
  • linhas 41–44: se a solicitação HTTP terminar com uma exceção, registe-a;
  • linhas 46–49: se a exceção [ex] não for nula, então é emitida para o observador. Não há necessidade aqui de chamar o método [onCompleted] para indicar ao observador que não serão emitidos mais elementos. Isto é implícito;

A principal conclusão destas explicações é que:

  • o método genérico [<T> Observable<T> getResponse(final IRequest<T> request)] retorna um tipo [Observable<T>] que emite um único elemento do tipo T ou uma exceção;
  • este método aceita como único parâmetro um tipo [IRequest<T>] cujo único método [getResponse()] executa a solicitação HTTP que retorna o tipo [Response<T>];

1.17.6. A classe [Dao]

A classe [Dao] evolui da seguinte forma:


@EBean
public class Dao extends AbstractDao implements IDao {
 
  // service customer REST
  @RestService
  protected WebClient webClient;
 
  // timeout before request execution
  private int delay;
  // debug mode
  private boolean isDebugEnabled;
  // class name
  private String className;
 
  // manufacturer
  public Dao() {
    // class name
    className = getClass().getSimpleName();
  }
 
 
  // interface IDao -------------------------------------------------------------------
  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // web client execution
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // waiting
        waitSomeTime(delay);
        // synchronous HTTP call
        return webClient.getAlea(a, b);
      }
    });
}
...
  • linha 2: a classe [Dao] estende a classe [AbstractDao];
  • linha 24: o método [getAlea] agora retorna um tipo [Observable<Integer>];
  • linha 30: chamada ao 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: o pedido HTTP é feito através da interface AA [webClient], tal como foi feito anteriormente. Sabemos que iremos recuperar um tipo [Response<Integer>], que é, de facto, o tipo que o método [IRequest<Integer>.getResponse()] deve devolver;
  • linha 36: aqui usamos uma funcionalidade chamada «closure»: a capacidade de encapsular valores externos a uma instância dentro dela quando esta é criada, neste caso os valores de [a, b] da linha 24. É isto que permite que o método [IRequest<Integer>.getResponse()] não tenha parâmetros. Estes valores foram incorporados no corpo do método. E onde normalmente alteraríamos os parâmetros do método (a, b) -> (x, y), aqui criamos uma nova instância de [IRequest<Integer>] encapsulando os valores de x e y;

1.17.7. A classe [MainActivity]

A classe [MainActivity], que implementa a interface [IDao], evolui da seguinte forma:


  // implémentation IDao --------------------------------------------------------------------
 
  @Override
  public Observable<Integer> getAlea(int a, int b) {
    // execution
    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() {
    // delete previous answers
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // reset the response counter to 0
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
    // activity initialization
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // we ask for the random numbers
    getAleasInBackground(a, b);
    // we start waiting
    beginWaiting();
}
  • linha 18: solicitamos números aleatórios através do método [getAleasInBackground], assim denominado porque os números serão solicitados numa thread diferente da thread da interface do utilizador;

  private int nbReponses = 0;
  // subscriptions to observables
  private List<Subscription> abonnements;
 
// annotation [Background] unnecessary
  void getAleasInBackground(int a, int b) {
    // initially no response and no subscriptions
    nbReponses = 0;
    abonnements.clear();
    // prepare the observable
    Observable<Integer> response = Observable.empty();
    // merge the results of the various HTTP calls
    // they are executed on an I/O thread
    for (int i = 0; i < nbAleas; i++) {
      response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
    }
    // the cumulative observable will be observed on the UI thread
    response = response.observeOn(AndroidSchedulers.mainThread());
    try {
      // the observable is executed
      abonnements.add(response.subscribe(new Action1<Integer>() {
        @Override
        public void call(Integer alea) {
          // we add the information to the list of answers
          showInfo(alea);
        }
      }, new Action1<Throwable>() {
        @Override
        public void call(Throwable th) {
          // error message
          showAlert(th);
          // end waiting
          doAnnuler();
        }
      }, new Action0() {
        @Override
        public void call() {
          // end waiting
          cancelWaiting();
        }
      }));
    } catch (RuntimeException e) {
      // the exception is displayed in the UiThread
      showAlert(e);
    }
}
  • linha 3: um observável tem assinantes. A ligação entre um assinante e o processo que este observa é chamada de subscrição. Aqui, teremos apenas um processo observado e um assinante. Portanto, teremos apenas uma subscrição. Por uma questão de princípio, estamos a tratá-lo como se pudéssemos ter vários processos observados monitorizados por diferentes observadores, o que resultaria em várias subscrições;
  • linhas 11–18: configuramos o processo observado (observável). É importante compreender que isto é apenas uma configuração: o processo não é executado;
  • linha 11: começamos com um observável vazio, um observável que não emite nada;
  • linhas 14–16: a este observável vazio, adicionamos [nbAleas] observáveis, que serão [nbAleas] pedidos HTTP que devolvem [nbAleas] números aleatórios;
  • Linha 15: Tal como anteriormente, o número aleatório i é solicitado à classe [MainActivity]. É importante compreender que ainda não foi executada nenhuma solicitação HTTP. O método [mainActivity.getRandom(a, b)] é executado e retorna um [Observable<Integer>]. Este é um processo que será observado assim que for iniciado;
  • linha 15: o método [subscribeOn(Schedulers.io())] solicita que o processo seja executado (quando for) numa thread de E/S. A biblioteca RxJava oferece diferentes tipos de threads. A thread de E/S é adequada para pedidos HTTP;
  • linha 15: o observável #i é fundido com o observável inicial da linha 11: a partir de [nbAleas] observáveis, cada um emitindo um elemento, criamos 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 as suas próprias notificações [onCompleted]. Isto poupa-nos de ter de contar as respostas, como fizemos na versão anterior, para determinar se recebemos todos os números esperados;
  • linha 18: a esta altura, já configurámos um observável que é a composição de [nbAleas] observáveis, cada um a ser executado numa thread de E/S;
  • linha 18: o método [observeOn(AndroidSchedulers.mainThread())] especifica em que thread os valores emitidos pelo observável devem ser observados. Aqui, a thread [AndroidSchedulers.mainThread())] pertence à biblioteca RxAndroid, não à RxJava. Refere-se à thread da interface do utilizador, também conhecida como loop de eventos. Este ponto é importante: numa aplicação Android, a modificação de um componente da interface do utilizador só pode ser feita na thread da interface do utilizador; caso contrário, ocorre uma exceção;
  • linhas 19–45: agora que o processo a ser observado foi configurado, executamo-lo;
  • linha 21: a operação [Observable.subscribe] inicia a execução do processo observado. Esta operação irá lançar os processos assíncronos [nbAleas] configurados anteriormente. Os resultados destes processos serão automaticamente disponibilizados ao observador na thread da IU;
  • Lembre-se 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á mais emitir;

O método [Observable.subscribe] recebe três objetos como parâmetros: [Action1<Integer>, Action1<Throwable>, Action0], cujos métodos [call] são utilizados para tratar cada um destes três eventos;

  • linhas 21–27: o primeiro parâmetro do tipo [Action1<Integer>] é utilizado para tratar o evento [onNext]. O seu método [call] recebe o elemento emitido pelo observável (linha 23);
  • linha 25: reutilizamos o método [showInfo] do exemplo anterior;
  • linhas 27–35: o segundo parâmetro do tipo [Action1<Throwable>] é utilizado para tratar o evento [onError]. O seu método [call] recebe a exceção emitida pelo observável (linha 29);
  • linha 31: reutilizamos o método [showAlert] do exemplo anterior;
  • linha 33: iniciamos o procedimento para cancelar o pedido do utilizador. Isto envolve o cancelamento de todos os observáveis que estão atualmente em execução;
  • linhas 35–41: o terceiro parâmetro do tipo [Action0] é utilizado para tratar o evento [onCompleted]. O seu método [call] não recebe parâmetros;
  • linha 39: a espera é cancelada;

O método [showInfo] evolui da seguinte forma:


  // annotation [UiThread] unnecessary
  protected void showInfo(int alea) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    if (!hasBeenCanceled) {
      // one more piece of information
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // we add the information to the list of answers
      reponses.add(0, String.valueOf(alea));
      // display answers
      adapterReponses.notifyDataSetChanged();
    }
}

O método tem duas alterações:

  • linha 1: removemos a anotação AA [@UiThread];
  • já não contamos as respostas para determinar se devemos ou não parar de esperar. Agora é o evento [onCompleted] do observável que fornece esta informação;

O método [showAlert] sofre as seguintes alterações:


  // annotation [UiThread] unnecessary
  protected void showAlert(Throwable th) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    if (!hasBeenCanceled) {
      // we cancel everything
      doAnnuler();
      // we display it
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}
  • A única alteração está na linha 1: removemos a anotação AA [@UiThread];

Por fim, o método [doAnnuler] é alterado da seguinte forma:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // memory
    hasBeenCanceled = true;
    // asynchronous tasks are cancelled
    if (abonnements != null) {
      for (Subscription abonnement : abonnements) {
        abonnement.unsubscribe();
      }
    }
    // end of wait
    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 (secção 1.16.1.7), inicie o cliente Android e repita os testes que realizou com o exemplo anterior (secção 1.16.2.8).

1.17.10. Tratamento do cancelamento

Repetimos os mesmos testes do exemplo anterior (secção 1.16.2.9).

Teste 1

Solicitamos 5 números, apesar de o servidor não ter sido iniciado. Obtemos os seguintes registos:

1
2
3
4
5
6
7
06-07 05:48:09.790 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:11.789 28272-28272/exemples.android D/Vue1Fragment_: Exception reçue
06-07 05:48:11.789 28272-28272/exemples.android D/Vue1Fragment_: Annulation demandée

Após a linha 7, não há mais registos, o que indica que o observador (Vue1Fragment) já não está a receber notificações do processo observado.

Teste 2

Agora, vamos iniciar o servidor e solicitar 5 números com um atraso de 5 segundos; em seguida, clique em [Cancelar] antes que o atraso termine. Os registos são os seguintes:

1
2
3
4
5
6
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:23.485 28272-28272/exemples.android D/Vue1Fragment_: Annulation demandée

Após a linha 6, não há mais registos, o que indica que o observador (Vue1Fragment) já não está a receber notificações do processo observado.

Este é o comportamento esperado de um cancelamento. Podemos, portanto, remover a variável booleana [hasBeenCanceled] do código [Vue1Fragment] que introduzimos no exemplo anterior, uma vez que o cancelamento não estava a funcionar como esperado.

O facto de o observador já não receber notificações após o observável ser cancelado não significa que as próprias solicitações HTTP sejam canceladas. Para verificar isto, modificamos a classe [Dao] da seguinte forma:


  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // web client execution
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // waiting
        waitSomeTime(delay);
        // synchronous HTTP call
        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 solicitação HTTP da linha 14;

Os registos do teste n.º 2 são os seguintes:

06-07 06:03:20.778 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.784 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:21.493 27085-27085/exemples.android D/Vue1Fragment_: Annulation demandée
06-07 06:03:21.636 27085-27440/exemples.android D/Dao_: response [{"body":176,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27442/exemples.android D/Dao_: response [{"body":145,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27439/exemples.android D/Dao_: response [{"body":197,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27438/exemples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27441/exemples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
  • linhas 1-5: foram feitas as 5 solicitações;
  • linha 6: o utilizador cancelou;
  • linhas 7-11: recebemos com sucesso as respostas às cinco solicitações HTTP. No entanto, como o observável foi cancelado, estes elementos não são passados para o observador;

1.17.11. Conclusão

No restante deste documento, as aplicações cliente/servidor serão implementadas utilizando a biblioteca RxAndroid em vez da biblioteca AA, pelas seguintes razões:

  1. A RxAndroid pode ser utilizada numa aplicação Android que não utilize a AA;
  2. A RxAndroid faz mais do que apenas facilitar 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;
  3. Assim que se tenta derivar uma classe anotada pela AA, como um fragmento, surgem problemas graves. É então necessário abandonar a AA e utilizar a Solução 1 para a programação assíncrona;

Os leitores interessados em explorar mais a fundo as capacidades da biblioteca RxAndroid podem consultar o documento [Introdução ao RxJava. Aplicação em ambientes Swing e Android]. Este utiliza o RxAndroid sem a biblioteca AA.

1.18. Exemplo-17: Componentes de Introdução de Dados

Iremos criar um novo projeto para demonstrar alguns componentes comuns utilizados em formulários de introdução de dados.

1.18.1. Criação do projeto

Duplicamos o projeto [Exemplo-13] para [Exemplo-17]:

O novo projeto terá apenas uma vista [view1.xml]. Por isso, iremos eliminar a vista [view2.xml] e o seu fragmento associado [View2Fragment] [2]. Iremos refletir esta alteração no gestor de fragmentos da [MainActivity]:


  // our fragment manager to be redefined for each application
  // must define the following methods: getItem, getCount, getPageTitle
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private final Fragment[] fragments = {new Vue1Fragment_()};
....
}

Execute novamente o projeto. Deverá exibir a vista n.º 1, tal como anteriormente. Vamos trabalhar a partir deste projeto.

1.18.2. A visualização XML do formulário

  

A visualização gerada pelo ficheiro [vue1.xml] é a seguinte:

Image

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:

  • Linha 2: um layout vertical [ScrollView]. Permite
  • exibir um formulário maior do que o ecrã do tablet
  • . Pode visualizar o formulário na totalidade através da
  • rolar;
 
  • linhas 125–132: uma caixa de seleção
  • linhas 134–159: um grupo de três botões de opção
  • linhas 161–166: uma barra de pesquisa
  • linhas 16–176: um campo de entrada de texto
  • linhas 178–186: um botão de seleção sim/não
  • linhas 188–195: um campo de introdução de hora
  • linhas 197–207: uma caixa de texto multilinha
  • linhas 209–215: uma lista suspensa
  • linhas 217-225: um campo de entrada de data
  • Todos os outros componentes são [TextView]s que exibem texto.
 

1.18.3. As strings do formulário

As strings 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>
  <!-- vue 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 [View1Fragment] é 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;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // the fields of the view displayed by the fragment
  @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;
 
  // drop-down list
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;
 
  @AfterViews
  void afterViews() {
    // check the first button
    radioButton1.setChecked(true);
    // the calendar
    datePicker1.setCalendarViewShown(false);
    // on seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
 
      public void onStopTrackingTouch(SeekBar seekBar) {
      }
 
      public void onStartTrackingTouch(SeekBar seekBar) {
      }
 
      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // the drop-down list
    list = new ArrayList<>();
    list.add("list 1");
    list.add("list 2");
    list.add("list 3");
  }
 
 
  @SuppressLint("DefaultLocale")
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    ...
  }
 
@Override
  protected void updateFragment() {
    // initialize drop-down list adapter
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    dropDownList.setAdapter(dataAdapter);
  }
}
  • linhas 22–49: recuperamos as referências de todos os componentes do formulário XML [view1] (linha 18);
  • linha 58: o método [setChecked] permite marcar um botão de opção ou uma caixa de seleção;
  • linha 60: por predefinição, o componente [DatePicker] apresenta tanto um campo de introdução de data como um calendário. A linha 60 remove o calendário;
  • linha 62: [SeekBar].setMax() define o valor máximo do controlo deslizante. O valor mínimo é 0;
  • linhas 63–74: Tratamos os eventos da barra de deslizamento. Para cada alteração feita pelo utilizador, queremos exibir o valor do controlo deslizante no [TextView] na linha 49;
  • linha 71: o parâmetro [progress] representa o valor do controle deslizante;
  • linhas 76–79: uma lista de [String]s que serão associadas à lista suspensa;
  • linha 90: o método [updateFragment] do fragmento. Quando é executado, a variável [activity] da classe pai já foi inicializada;
  • linha 92: a fonte de dados [list] está ligada ao adaptador da lista suspensa;
  • linhas 93–94: o [dataAdapter] está vinculado à lista suspensa [dropDownList];
  • linha 84: o método [doValider] está associado a um clique no botão [Valider];

O objetivo do método [doValider] é exibir os valores introduzidos pelo utilizador. O seu código é o seguinte:


  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // list of messages to display
    List<String> messages = new ArrayList<>();
    // checkbox
    boolean isChecked = checkBox1.isChecked();
    messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
    // radio buttons
    int id = radioGroup.getCheckedRadioButtonId();
    String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
    messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
    // on SeekBar
    int progress = seekBar.getProgress();
    messages.add(String.format("SeekBar [value=%d]", progress));
    // the input field
    String texte = String.valueOf(saisie.getText());
    messages.add(String.format("Saisie simple [value=%s]", texte));
    // the switch
    boolean état = switch1.isChecked();
    messages.add(String.format("Switch [value=%s]", état));
    // the date
    int an = datePicker1.getYear();
    int mois = datePicker1.getMonth() + 1;
    int jour = datePicker1.getDayOfMonth();
    messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
    // multi-line text
    String lignes = String.valueOf(multiLignes.getText());
    messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
    // by the hour
    int heure = timePicker1.getHour();
    int minutes = timePicker1.getMinute();
    messages.add(String.format("Heure [%d, %d]", heure, minutes));
    // drop-down list
    int position = dropDownList.getSelectedItemPosition();
    String selectedItem = String.valueOf(dropDownList.getSelectedItem());
    messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
    // display
    doAfficher(messages);
}
  • linha 4: os valores introduzidos serão adicionados a uma lista de mensagens;
  • linha 6: o método [CheckBox].isChecked() determina se uma caixa de seleção está marcada ou não;
  • linha 9: o método [RadioGroup].getCheckedButtonId() devolve o ID do botão de opção selecionado ou -1 se nenhum estiver selecionado;
  • linha 10: o código [activity.findViewById(id)] recupera o botão de opção marcado e, consequentemente, o seu rótulo;
  • linha 13: o método [SeekBar].getProgress() devolve o valor de um controlo deslizante;
  • linha 19: o método [Switch].isChecked() determina se um interruptor está ligado (true) ou desligado (false);
  • linha 22: o método [DatePicker].getYear() recupera o ano selecionado utilizando um objeto [DatePicker];
  • linha 23: o método [DatePicker].getMonth() devolve o mês selecionado a partir de um objeto [DatePicker] dentro do intervalo [0,11];
  • linha 24: o método [DatePicker].getDayOfMonth() devolve o dia do mês selecionado utilizando um objeto [DatePicker] dentro do intervalo [1,31];
  • linha 30: o método [TimePicker].getHour() devolve a hora selecionada utilizando um objeto [TimePicker];
  • linha 31: o método [TimePicker].getMinute() devolve os minutos selecionados utilizando um objeto [TimePicker];
  • linha 34: o método [Spinner].getSelectedItemPosition() devolve a posição do item selecionado numa lista suspensa;
  • linha 35: o método [Spinner].getSelectedItem() devolve o item selecionado numa lista suspensa;

O método [doAfficher], que exibe a lista de valores introduzidos, é o seguinte:


    private void doAfficher(List<String> messages) {
        // we build the poster text
        StringBuilder texte = new StringBuilder();
        for (String message : messages) {
            texte.append(String.format("%s\n", message));
        }
        // we display it
        new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
  • linha 1: o método recebe uma lista de mensagens para exibir;
  • linhas 3–6: um objeto [StringBuilder] é construído a partir dessas mensagens. Para a concatenação de cadeias de caracteres, o tipo [StringBuilder] é mais eficiente do que o tipo [String];
  • linha 8: uma caixa de diálogo exibe o texto da linha 3:

Image

1.18.5. Executar o projeto

Execute o projeto e teste os vários componentes de entrada.

1.19. Exemplo-18: Utilização de um padrão de visualização

1.19.1. Criação do projeto

Criamos um novo projeto [Exemplo-18] copiando o projeto [Exemplo-13].

1.19.2. O modelo de vista

Queremos reutilizar as duas vistas do projeto e incluí-las num modelo:

  

Image

Cada uma das duas vistas terá a mesma estrutura:

  • em [1], um cabeçalho;
  • em [2], uma coluna à esquerda que poderá conter links;
  • em [3], um rodapé;
  • em [4], o conteúdo.

Isto é conseguido modificando a vista base da atividade [activity_main.xml];

O código XML para a 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] é gerado pelas linhas 38–54;
  • o painel esquerdo [2] é gerado pelas linhas 56–84;
  • o rodapé [3] é criado pelas linhas 86–101;
  • o conteúdo [4] é gerado pelas linhas 78–84;

A vista XML [principal] utiliza informações contidas 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 seguinte ficheiro [strings.xml]:


<?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 item de uma lista. A vista repetida pode ter qualquer grau de complexidade, desde uma simples cadeia de caracteres até uma vista que permita introduzir informações para cada item da lista. Iremos criar o seguinte [ListView]:

Image

Cada vista na lista tem três componentes:

  • um [TextView] para informações;
  • um [CheckBox];
  • um [TextView] clicável;

1.20.1. Criação do projeto

Criamos um novo projeto [Exemplo-19] clonando o projeto [Exemplo-18].

  

Iremos desenvolver o projeto conforme descrito em [3].

1.20.2. A sessão

  

A sessão armazena 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 {
  // a list of data
  private List<Data> liste=new ArrayList<>();
 
  // getters and setters
...
}
  • linha 11: a lista de dados utilizada por ambas as vistas;

A classe [Data] é a seguinte:


package exemples.android.architecture;
 
public class Data {
 
    // data
    private String texte;
    private boolean isChecked;
 
    // manufacturer
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }
 
    // getters and setters
    ...
}
  • linha 6: o texto que irá preencher o primeiro [TextView] de cada item da lista;
  • linha 7: o valor booleano que será usado para marcar ou desmarcar a [checkBox] de cada item da lista;

1.20.3. A [MainActivity]

O código para o método [@AfterInject] fica assim:


  // injection session
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // create a list of data
    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 [View1]

A vista XML [view1.xml] exibe a área [1] acima. 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 {
 
  // the fields of the view displayed by the fragment
  @ViewById(R.id.listView1)
  protected ListView listView;
  // list adapter
  private ListAdapter adapter;
  // init done
  private boolean initDone = false;
 
  @AfterViews
  void afterViews() {
    // memory
    afterViewsDone = true;
  }
 
  @Click(R.id.button_vue2)
  void navigateToView2() {
    // navigate to view 2
    mainActivity.navigateToView(1);
  }
 
  public void doRetirer(int position) {
   ...
  }
 
  @Override
  protected void updateFragment() {
    if (!initDone) {
      // associate data with [ListView]
      adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
      initDone = true;
    }
    // if the fragment has been (re)generated - in this case the ListView must be reconnected to its adapter
    listView.setAdapter(adapter);
    // if other fragments have changed the data source - in this case, refresh the ListView
    adapter.notifyDataSetChanged();
  }
}
  • linha 15: a vista XML [view1] está associada ao fragmento;
  • linhas 26–30: o método [@AfterViews] não faz nada. No entanto, é necessário definir a variável [afterViewsDone] como true, porque é utilizada pela classe pai [AbstractFragment];
  • linhas 42–53: o método [updateFragment], que é chamado sempre que o fragmento se torna 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. Este não é o caso aqui, mas seria se a aplicação tivesse 3 fragmentos com uma adjacência de 1;
  • linha 44: o adaptador [ListView] só precisa de ser inicializado uma vez;
  • linha 46: associamos um [ListAdapter] a este [ListView]. Iremos criar esta classe. Ela deriva da classe [ArrayAdapter], que já utilizámos para associar dados a um [ListView]. Passamos várias informações ao construtor [ListAdapter]:
    • uma referência à atividade atual,
    • o identificador da vista que será instanciada para cada item da lista,
    • uma fonte de dados para preencher a lista,
    • uma referência ao fragmento. Isto será utilizado para tratar um clique num link [Remove] no [ListView] através do método [doRemove] na linha 38;
  • Linha 50: O adaptador é vinculado ao [ListView]. Ao mesmo tempo, a fonte de dados [lists] é vinculada ao [ListView]. Esta operação é realizada aqui sempre que a vista n.º 1 é exibida. Na realidade, só precisa de ser feita uma vez que o método [@AfterViews] tenha sido executado. Aqui, a instrução é executada com demasiada frequência. Precisamos de uma variável booleana que nos indique que o método [@AfterViews] acabou de ser executado e que o [ListView] deve, portanto, ser reassociado ao seu adaptador;
  • Linha 52: Atualizamos o [ListView]. Neste exemplo, isto não serve para nada, porque apenas a Vista #1 pode modificar a fonte de dados do [ListView]. Vamos considerar um caso mais geral em que a vista #2 também poderia alterar a fonte de dados da [ListView]. Encontraremos exemplos desse tipo mais adiante neste documento. Neste caso, ao mudar da vista #2 para a vista #1, a [ListView] na vista #1 deve ser atualizada;

1.20.7. O [ListAdapter] do [ListView]

A classe [ListAdapter]

  • configura a fonte de dados do [ListView];
  • gerencia a exibição dos vários elementos no [ListView];
  • trata os eventos desses elementos;

O seu código é o seguinte:


package exemples.android.fragments;
 
import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {
 
    // execution context
    private Context context;
    // the id of the layout displaying a line in the list
    private int layoutResourceId;
    // list data
    private List<Data> data;
    // the fragment that displays the [ListView]
    private Vue1Fragment fragment;
    // the adapter
    final ListAdapter adapter = this;
 
    // manufacturer
    public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
        super(context, layoutResourceId, data);
        // memorize information
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.data = data;
        this.fragment = fragment;
    }
 
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
...
    }
}
  • linha 5: a classe [ListAdapter] estende a classe [ArrayAdapter];
  • linha 19: o construtor;
  • linha 20: não se esqueça de chamar o construtor da classe pai [ArrayAdapter] com os três primeiros parâmetros;
  • linhas 22–25: armazenamos as informações do construtor;
  • linha 29: o método [getView] será chamado repetidamente pelo [ListView] para gerar a vista para o elemento #[position]. O resultado [View] devolvido é uma referência à vista criada.

O código para o método [getView] é o seguinte:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // create the current ListView line
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // the text
        TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
        textView.setText(data.get(position).getTexte());
        // the checkbox
        CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
        checkBox.setChecked(data.get(position).isChecked());
        // the [Remove] link
        TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
        txtRetirer.setOnClickListener(new OnClickListener() {
 
            public void onClick(View v) {
                fragment.doRetirer(position);
            }
        });
        // manage the click on the checkbox
        checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
 
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                data.get(position).setChecked(isChecked);
            }
        });
        // we return the line
        return row;
}
  • Linha 2: O método recebe três parâmetros. Vamos usar apenas o primeiro;
  • linha 4: criamos a vista para o elemento #[position]. Esta é a vista [list_data] cujo ID foi passado como segundo parâmetro para o construtor. Em seguida, recuperamos as referências aos componentes da vista que acabámos de instanciar;
  • linha 6: recuperamos a referência ao [TextView] n.º 1;
  • linha 7: atribuímos-lhe texto a partir da fonte de dados que foi passada como terceiro parâmetro para o construtor;
  • linha 9: recuperamos a referência ao [CheckBox] #2;
  • linha 10: marcamo-la ou desmarcamo-la utilizando um valor da fonte de dados da [ListView];
  • linha 12: recuperamos a referência ao [TextView] #3;
  • linhas 13–18: tratamos o clique no link [Remove];
  • linha 16: o método [Vue1Fragment].doRetirer trata deste clique. Faz mais sentido que o fragmento que exibe o [ListView] trate deste evento. Ele tem uma visão geral que a classe [ListAdapter] não possui. A referência ao fragmento [Vue1Fragment] foi passada como quarto parâmetro para o construtor da classe;
  • Linhas 20–25: Lida com o clique na caixa de seleção. A ação realizada nela reflete-se nos dados que exibe. Isto deve-se ao seguinte motivo: O [ListView] é uma lista que exibe apenas uma parte dos seus itens. Assim, um item da lista está por vezes oculto e por vezes exibido. Quando o elemento #i precisa de ser exibido, o método [getView] da linha 2 acima é chamado para a posição #i. A linha 10 irá recalcular o estado da caixa de seleção com base nos dados aos quais está ligada. Portanto, deve armazenar o estado da caixa de seleção ao longo do tempo;

1.20.8. Remover um item da lista

O clique no link [Remove] é tratado no fragmento [Vue1Fragment] pelo seguinte método [doRetirer]:


  public void doRetirer(int position) {
    // remove element n° [position] from the list
    List<Data> liste = mainActivity.getListe();
    liste.remove(position);
    // note the scroll position to return to it
    // read
    // [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // position of 1st element fully visible or not
    int firstPosition = listView.getFirstVisiblePosition();
    // y offset of this element relative to the top of the ListView
    // measures the height of any hidden part
    View v = listView.getChildAt(0);
    int top = (v == null) ? 0 : v.getTop();
    // refresh the [ListView]
    adapter.notifyDataSetChanged();
    // we position ourselves at the right spot on the ListView
    listView.setSelectionFromTop(firstPosition, top);
}
  • linha 1: Obter a posição na [ListView] do link [Remover] que foi clicado;
  • linha 3: recupera a lista de dados;
  • linha 4: remova o item na posição [position];
  • linha 15: atualizamos o [ListView]. Sem isto, nada muda visualmente.
  • Linhas 5–13, 17: um processo bastante complexo. Sem ele, acontece o seguinte:
    • o [ListView] exibe as linhas 15–18 da lista de dados,
    • a linha 16 é eliminada,
    • a linha 15 acima reinicia-a completamente, e o [ListView] exibe então as linhas 0–3 da lista de dados;

Com as linhas acima, a eliminação ocorre e o [ListView] permanece posicionado na linha seguinte à linha eliminada.

1.20.9. A vista XML [View2]

O código XML para a 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: componente [TextView] n.º 1;
  • linhas 26–33: componente [TextView] n.º 2;
  • linhas 17-24: componente [Button] #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 {
 
    // fields of view
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;
 
    @AfterViews
    void initFragment(){
        // memory
        afterViewsDone=true;
    }
 
  @Click(R.id.button_vue1)
    void navigateToView1() {
        // navigate to view 1
        mainActivity.navigateToView(0);
    }
 
    @Override
    protected void updateFragment() {
        // displays list items selected in view 1
        StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
        for (Data data : mainActivity.getListe()) {
            if (data.isChecked()) {
                texte.append(String.format("(%s)", data.getTexte()));
            }
        }
        texte.append("]");
        txtResultats.setText(texte);
    }
}

O código importante está no método [updateFragment] na linha 32:

  • linha 34: calculamos o texto a ser exibido no [TextView] n.º 2;
  • linhas 35–39: percorremos a lista de dados exibidos pelo [ListView]. Esta está armazenada na atividade;
  • linha 36: se o item de dados i tiver sido marcado, o rótulo associado é adicionado a um [StringBuilder];
  • linha 41: o [TextView] exibe o texto calculado;

1.20.11. Execução

Crie uma configuração de execução para este projeto e execute-o.

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 {
 
    // data
    private String texte;
    private boolean isChecked;
 
    // manufacturer
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }
...
 
}

Na linha 7, utilizámos uma variável booleana para gerir as caixas de seleção dos itens na [ListView]. Muitas vezes, a [ListView] precisa de apresentar dados que podem ser selecionados marcando uma caixa, mesmo que o item na fonte de dados não tenha um campo booleano correspondente a essa caixa. Nesse caso, pode proceder da seguinte forma:

A classe [Data] passa a ter o seguinte aspeto:


package exemples.android.fragments;
 
public class Data {
 
    // data
    private String texte;
 
    // manufacturer
    public Data(String texte) {
        this.texte = texte;
    }
 
    // getters and setters
...
}

Criamos uma classe [CheckedData] derivada da anterior:


package exemples.android.fragments;
 
public class CheckedData extends Data {
 
    // checked item
    private boolean isChecked;
 
    // manufacturer
    public CheckedData(String text, boolean isChecked) {
        // parent
        super(text);
        // local
        this.isChecked = isChecked;
    }
 
    // getters and setters
...
}

Em seguida, basta substituir o tipo [Data] pelo tipo [CheckedData] em todo o código (MainActivity, ListAdapter, View1Fragment, View2Fragment). Por exemplo, em [MainActivity]:


  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // create a list of data
    List<CheckedData> liste = session.getListe();
    for (int i = 0; i < 20; i++) {
      liste.add(new CheckedData("Texte n° " + i, false));
    }
}

O projeto para esta versão é fornecido com o nome [Exemplo-19B].

1.21. Exemplo-20: Utilização de um menu

1.21.1. Criação do projeto

Duplicamos o projeto [Exemplo-19B] para o projeto [Exemplo-20]:

3

Vamos remover os botões das vistas 1 e 2 e substituí-los 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 para a 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 itens do menu são definidos pelas seguintes informações:

  • android:id: o identificador do elemento;
  • android:title: o rótulo do item;
  • app:showsAsAction: indica se o item do menu pode ser colocado na barra de ação da atividade. [ifRoom] indica que o item deve ser colocado na barra de ação se houver espaço para tal;
  • uma opção de menu pode ser, ela própria, um submenu (a tag <menu>, linhas 25, 29);

O ficheiro [res / menu / menu_vue2] define o menu para a 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. Gestão do menu na classe abstrata [AbstractFragment]

Vamos separar a gestão do menu para a 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 {
 
  // data accessible to daughter classes
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  protected String className;
 
  // activity
  protected IMainActivity mainActivity;
  protected Activity activity;
 
  // session
  protected Session session;
 
  // menu
  private Menu menu;
  private int[] menuOptions;
  private boolean initDone;
 
  // manufacturer
  public AbstractFragment() {
    // init
    className = getClass().getSimpleName();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }
 
@Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // memory
    this.menu = menu;
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    // retrieve # menu options if not already done
    if (!initDone) {
      // retrieve the # menu options
      List<Integer> menuOptionsIds = new ArrayList<>();
      getMenuOptions(menu, menuOptionsIds);
      // transfer the list of options to a table
      menuOptions = new int[menuOptionsIds.size()];
      for (int i = 0; i < menuOptions.length; i++) {
        menuOptions[i] = menuOptionsIds.get(i);
      }
      // activity
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = this.mainActivity.getSession();
      // memory
      initDone = true;
    }
 
    // the girl fragment is asked to stand
    updateFragment();
  }
 
 
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
   ...
  }
 
  // display menu options -----------------------------------
  protected void setAllMenuOptions(boolean isVisible) {
    ....
  }
 
  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    ...
  }
 
  // update girl class
  protected abstract void updateFragment();
}
  • linha 42: os registos mostram que o método [onCreateOptionsMenu] é chamado sempre que o fragmento é apresentado. É chamado muito tarde, especificamente depois de o método [updateFragment] ter sido chamado. Isto sugere que poderia ser usado 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 nos permite criar o menu a partir da sua descrição inicial. Não vamos usar esta opção aqui porque vamos usar uma anotação AA que fará isso por nós;
  • linha 44: guardamos o menu. Vamos precisar dele mais tarde;
  • linhas 52–53: guardamos os IDs de todos os itens do menu na matriz da linha 28;
  • 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: guardamos a atividade como uma instância da classe [Activity] do Android;
  • linha 56: guardamos a atividade como uma instância da interface [IMainActivity];
  • linha 57: armazenamos a sessão;
  • linha 59: observamos que a classe já foi inicializada, pelo que não precisamos de o fazer novamente (linha 50);
  • linha 63: solicitamos ao fragmento filho que se atualize. Isto é possível porque o fragmento está visível e associado à sua vista e ao seu menu;

O método [getMenuOptions], que recupera os IDs dos itens do menu, é o seguinte:


  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // scroll through all menu items
    for (int i = 0; i < menu.size(); i++) {
      // item n° i
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // if item n° i is a sub-menu, then start again
      if (menuItem.hasSubMenu()) {
        // recursivity
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
}

O método [setAllMenuOptions] permite ocultar ou mostrar todas as opções do menu;


  protected void setAllMenuOptions(boolean isVisible) {
    // update all menu options
    for (int menuItemId : menuOptions) {
      menu.findItem(menuItemId).setVisible(isVisible);
    }
}

O método [setMenuOptions] permite ocultar ou mostrar determinadas opções do menu;


  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    // update certain menu options
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}

A classe [MenuItemState] é a seguinte:

  

package exemples.android.architecture;
 
public class MenuItemState {
 
  // menu option identify
  private int menuItemId;
  // option visibility
  private boolean isVisible;
 
  // manufacturers
  public MenuItemState() {
 
  }
 
  public MenuItemState(int menuItemId, boolean isVisible) {
    this.menuItemId = menuItemId;
    this.isVisible = isVisible;
  }
 
  // getters and setters
...
}

1.21.4. Gestão do menu no fragmento [View1Fragment]

A classe [Vue1Fragment] passa a ter o seguinte aspeto:


@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {
 
...
 
  @OptionsItem(R.id.navigationVue2)
  void navigateToView2() {
    // navigate to view 2
    mainActivity.navigateToView(1);
  }
 
  @OptionsItem(R.id.actionValider)
  void valider() {
    // a message is displayed
    Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
  }
 
  private boolean actionCacherMontrerTout = true;
  @OptionsItem(R.id.actionCacherMontrerTout)
  void cacherMontrerTout() {
    // we change state
    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() {
    // we change state
    actionCacherMontrerActions = !actionCacherMontrerActions;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
  }
 
  private boolean actionCacherMontrerActionsValider = true;
  @OptionsItem(R.id.actionCacherMontrerActionsValider)
  void actionCacherMontrerActionsValider() {
    // we change state
    actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
  }
...
 
  @Override
  protected void updateFragment() {
    ....
    // update the menu
    //setMenuOptions(...)
  }
}
  • linha 2: o menu [res/menu/menu_vue1.xml] está 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)] identifica o método que deve ser executado quando a opção de menu [Navegação / Vista 2] é clicada;
  • linhas 19–25: para ocultar um ramo do menu, basta ocultar a sua opção raiz;
  • linha 24: as opções raiz [menuNavigation, menuActions] são mostradas ou ocultadas;
  • linha 40: para mostrar uma opção num ramo do menu, deve não só mostrar essa opção, mas também todas as opções encontradas ao mover-se da opção folha de volta para a raiz do menu;

1.21.5. Gestão do menu no fragmento [Vue2Fragment]

Código semelhante pode ser encontrado no fragmento da View 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 {
 
  // fields of view
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;
 
  @OptionsItem(R.id.navigationVue1)
  void navigateToView1() {
    // navigate to view 1
    mainActivity.navigateToView(0);
  }
 
  @Override
  protected void updateFragment() {
    // displays list items selected in view 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);
    // update the menu
    // setMenuOptions(...)
  }
}
  • linha 35: exibe a opção [Navegação / Vista 1];
  • linhas 17-20: quando a opção [Navegação / Vista 1] é clicada, o método [navigateToView1] é chamado;

1.21.6. Execução

Crie um contexto de tempo de execução para este projeto e execute-o.

1.22. Exemplo-21: Refatoração da classe [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 está prestes a ser exibido;
  • quando é chamado, as associações do fragmento com a sua atividade, vista e menu são estabelecidas;

Para demonstrar isto, vamos revisitar o Exemplo 12, que apresenta muitos fragmentos cuja adjacência pode ser modificada. Nesse exemplo, os fragmentos não tinham um menu. Vamos associar-lhes um menu vazio.

1.22.1. Criação do projeto

Duplicamos o projeto [Exemplo-12] para o projeto [Exemplo-21]:

1.22.2. O menu de fragmentos

  

O menu adicionado para os fragmentos estará vazio:


<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context="exemples.android.MainActivity">
</menu>

O que é preciso 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á possui um menu, o menu associado aos fragmentos é adicionado ao menu da atividade: assim, tem as opções de ambos os menus. Aqui, o menu dos fragmentos estará vazio. Por isso, só verá o menu da atividade.

1.22.3. Os fragmentos

  

Reutilizamos a classe abstrata [AbstractFragment] do exemplo anterior (ver secção 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 {

Em ambos os fragmentos [PlaceholderFragment] e [Vue1Fragment], removemos todas as referências à antiga classe abstrata [AbstractFragment].

1.22.4. Execução

Execute a aplicação e verifique se funciona. Verifique os registos para ver quando o método [onCreateOptionsMenu] da classe [AbstractFragment] é executado. Agora é este método que chama o método [updateFragment] dos fragmentos filhos.

1.23. Exemplo-22: Guardar/Restaurar o estado da atividade e dos fragmentos

1.23.1. O problema

Aqui abordamos a questão da rotação do dispositivo Android (vertical <--> horizontal). Para ilustrar isto, vamos revisitar o Exemplo 21 anterior:

Image

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

Image

Podemos ver que:

  • em [1], o separador [Fragmento 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:

07-13 04:08:27.188 1677-1677/exemples.android D/MainActivity: constructor
07-13 04:08:27.189 1677-1677/exemples.android D/MainActivity: afterInject
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.194 1677-1677/exemples.android D/MainActivity: afterViews
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.203 1677-1677/exemples.android D/PlaceholderFragment: afterViews 4 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.204 1677-1677/exemples.android D/PlaceholderFragment: afterViews 3 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.208 1677-1677/exemples.android D/Vue1Fragment: afterViews Vue1Fragment_ - numVisit=0
07-13 04:08:27.208 1677-1677/exemples.android D/PlaceholderFragment: afterViews 2 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.209 1677-1677/exemples.android D/PlaceholderFragment: afterViews 1 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.351 1677-1677/exemples.android D/menu: création menu en cours
07-13 04:08:27.351 1677-1677/exemples.android D/PlaceholderFragment_: création menu en cours
07-13 04:08:27.351 1677-1677/exemples.android D/PlaceholderFragment: update 3 - PlaceholderFragment_ - numVisit=0, initDone=true, getActivity()==null:false
  • linha 1: podemos ver 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 está prestes a ser apresentado. Vemos que, antes do incremento, a contagem de visitas é 0;

Podemos 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 [View 1]. Este é o separador que está visível;
  • Após a rotação do dispositivo, o gestor de páginas [mViewPager] volta a apresentar o mesmo fragmento, que neste caso é o fragmento n.º 3. É importante lembrar aqui que separadores e fragmentos são conceitos diferentes e têm ciclos de vida distintos. O método [updateFragment] do fragmento n.º 3 será executado:

  public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // increment visit no
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
  • Linha 7: O ID da última visita é lido a partir da sessão. No entanto, a sessão — tal como tudo o resto — foi reiniciada, e o ID da visita foi reiniciado para zero. Isto explica o resultado apresentado no fragmento n.º 3;

1.23.2. Métodos para guardar/restaurar a atividade e os fragmentos

1.23.2.1. Solução 1: Cópia de segurança manual

Quando o dispositivo gira, são chamados dois métodos da atividade:


// backup / restore management ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // parent
    super.onSaveInstanceState(outState);
    // backup activity status
    // ....
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
     // restoring activity
    // ...
  }
  • Linhas 2–8: O método [onSaveInstanceState] é chamado pelo sistema durante a rotação. É aqui que a atividade pode ser guardada. Se nada for feito, nada é guardado. O estado da atividade deve ser guardado no parâmetro [Bundle outState] passado para o 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 quando a atividade é criada. Se o estado da atividade tiver sido guardado, esse estado guardado é-lhe passado no parâmetro [Bundle savedInstanceState]. Para recuperar os valores guardados, estão disponíveis métodos como [getString, getInt, getLong, getBoolean, getChar, ...] com um único parâmetro: T getT(String key);

Os fragmentos dispõem destes mesmos dois métodos para guardar o seu estado.

Vamos usar esta informação para guardar e restaurar o estado do Exemplo 21. Para tal, duplicamos o projeto [Exemplo-21] para [Exemplo-22].

1.23.2.2. Solução 2: Gravação automática

A documentação do Android indica que, quando o dispositivo é rodado, é possível impedir que um fragmento seja destruído utilizando a instrução: [Fragment].setRetainInstance(true). Vários artigos no [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 (Secção 1.18 — uma aplicação de fragmento único que apresenta um formulário) e Exemplo-21 (Secção 1.22 — uma aplicação de cinco fragmentos). Em ambos os casos, aplicar esta única instrução a todos os fragmentos da aplicação revelou-se insuficiente para restaurar corretamente a vista apresentada quando o dispositivo foi rodado. Em vez de construir dois modelos, um baseado em [setRetainInstance(true)] e outro baseado em [setRetainInstance(false)] — que é o valor padrão —, decidi seguir as recomendações do [StackOverflow] e manter o valor padrão de false para o método [setRetainInstance(boolean)]. A instrução: [Fragment].setRetainInstance(true) nunca foi utilizada no resto deste documento.

1.23.3. O método de backup/restauração para o projeto [Exemplo-22]

O projeto [Exemplo-22] evolui da seguinte forma:

  

Aparecem duas novas classes:

  • [PlaceHolderFragmentState], que armazenará o estado de um fragmento do tipo [PlaceHolderFragment];
  • [Vue1FragmentState], que armazenará o estado de um fragmento do tipo [Vue1Fragment];

Estas classes são as seguintes:


package exemples.android;
 
public class Vue1FragmentState {
  // status Vue1Fragment
  private boolean hasBeenVisited=false;
  // getters and setters
...
}
  • linha 5: o booleano [hasBeenVisited] é verdadeiro se o fragmento [Vue1Fragment] tiver sido visitado (exibido) pelo menos uma vez. Este campo foi criado para o exemplo porque o fragmento [Vue1Fragment] não tem nada para guardar;

A classe [PlaceHolderFragmentState] é a seguinte:


package exemples.android;
 
public class PlaceHolderFragmentState {
  // whether visited or not
  private boolean hasBeenVisited;
  // display text
  private String text;
 
  // getters and setters
...
}
  • linha 5: vemos o booleano [hasBeenVisited];
  • linha 7: o texto exibido pelo fragmento no momento em que precisa de ser guardado. Vimos que este texto se perdeu durante a rotação;

O estado dos fragmentos será armazenado na sessão, e a atividade será responsável por guardar e 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 {
  // number of fragments visited
  private int numVisit;
  // n° fragment type [PlaceholderFragment] displayed in the second tab
  private int numFragment = -1;
  // selected tab no
  private int selectedTab = 0;
  // n° current view
  private int currentView;
 
  // fragment backups ---------------
  private Vue1FragmentState vue1FragmentState;
  private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];
 
  // manufacturer
  public Session() {
    for (int i = 0; i < placeHolderFragmentStates.length; i++) {
      placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
    }
    vue1FragmentState = new Vue1FragmentState();
  }
  // getters and 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, os campos das linhas 18 e 19 são inicializados;
  • linhas 12–15: surgem dois novos campos:
    • linha 13: o número da última guia selecionada;
    • linha 15: o número do último fragmento exibido;

A atividade guarda/restaura a sessão da seguinte forma:


  // backup / restore management ----------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // parent
    super.onSaveInstanceState(outState);
    // save session
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // log
    if (IS_DEBUG_ENABLED) {
      try {
        Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
      // session recovery
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // log
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    }
}
  • linha 8: a sessão é guardada como uma cadeia JSON;
  • linha 29: restaurar a sessão a partir da sua cadeia JSON;

Para gerir o armazenamento e a restauração de fragmentos, a classe abstrata [AbstractFragment] evolui da seguinte forma:


// backup / restore management -----------------------------------------------
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // backup?
    if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
      // the fragment will be hidden - save it
      saveFragment();
      saveFragmentDone = true;
    }
    // memory
    this.isVisibleToUser = isVisibleToUser;
  }
 
  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // parent
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // the fragment must be restored
    fragmentHasToBeInitialized = true;
  }
 
 
  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "onSaveInstanceState");
    }
    // parent
    super.onSaveInstanceState(outState);
    // save fragment only if visible
    if (isVisibleToUser && !saveFragmentDone) {
      saveFragment();
      saveFragmentDone = true;
    }
  }
 
  // girls' classes
  protected abstract void updateFragment();
 
  protected abstract void saveFragment();
  • Decidimos 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 o fragmento deve ser guardado e o fragmento está visível (linha 38);

Este mecanismo evita guardar com mais frequência do que o necessário. De facto, uma vez que guardámos o estado do fragmento i quando este passou de visível para oculto, quando o fragmento j é apresentado e ocorre uma rotação, não há necessidade de guardar o fragmento i novamente. Se não tiver sido apresentado novamente desde a sua última gravação, então o seu estado não se alterou. Apenas o estado do fragmento j precisa de ser guardado. Este mecanismo tem também outra vantagem: não é apenas durante a rotação do dispositivo que precisamos de guardar o estado de um fragmento. Há também o caso da navegação pura entre fragmentos, por exemplo, num sistema com separadores. Nesses casos, queremos recuperar um fragmento no estado em que se encontrava quando foi exibido pela última vez. Este estado pode ter desaparecido parcialmente se o fragmento tiver sido, em algum momento, removido da vizinhança dos fragmentos exibidos. O fragmento não é então totalmente reconstruído, mas a sua vista associada sim. O salvamento realizado quando o fragmento ficou oculto será usado para restaurar o último estado desta vista;

  • linhas 10, 40: para evitar efetuar dois salvamentos sucessivos, o booleano [saveFragmentDone] é utilizado para indicar que foi efetuado um salvamento;
  • 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 o booleano [fragmentHasToBeInitialized] como verdadeiro. Isto porque o fragmento filho precisa de saber que deve reinicializar totalmente o estado do fragmento a partir de um estado que irá encontrar na sessão;

Ainda na classe [AbstractFragment], o método [onCreateOptionsMenu] é alterado da seguinte forma:


// fragment update
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // memory
    this.menu = menu;
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    ...
    // the girl fragment is asked to update itself
    updateFragment();
    // backup to do
    saveFragmentDone = false;
  }
  • linha 14: vimos que o booleano [saveFragmentDone] foi definido como true quando foi efetuado um salvamento. A certa altura, tem de ser redefinido para false. Quando o método [updateFragment] (linha 12) do fragmento filho é executado, este torna-se visível. No entanto, é quando um fragmento está visível que deve ser guardado, especificamente no momento em que faz a transição do estado visível para o estado oculto. Definimos então o booleano [saveFragmentDone] para false para que a gravação possa ocorrer;

1.23.4. Guardar o fragmento [Vue1Fragment]

Os fragmentos são guardados no método [saveFragment] chamado pela classe pai [AbstractFragment]:


// save fragment status
  @Override
  public void saveFragment() {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
    }
    // in-session saving of fragment status
    Vue1FragmentState state = new Vue1FragmentState();
    state.setHasBeenVisited(true);
    session.setVue1FragmentState(state);
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • linhas 9–11: guardar o estado do fragmento na sessão. Quando o método [saveFragment] é chamado, o fragmento está visível. Portanto, o booleano [hasBeenVisited] deve ser definido como true (linha 10);

1.23.5. Guardar o fragmento [PlaceHolderFragment]

Os fragmentos são guardados no método [saveFragment] chamado pela classe pai [AbstractFragment]:


  @Override
  public void saveFragment() {
    // save fragment state in session
    PlaceHolderFragmentState state = new PlaceHolderFragmentState();
    state.setText(textViewInfo.getText().toString());
    state.setHasBeenVisited(true);
    session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • linhas 4–7: guarda o estado do fragmento na sessão;
  • linha 5: o texto atualmente exibido pelo [TextView] textViewInfo é guardado;
  • linha 6: o booleano [hasBeenVisited] do fragmento é definido como true;
  • linha 7: o estado do fragmento é guardado na sessão na matriz [placeHolderFragmentStates]. O índice do elemento a inicializar é o número da secção do fragmento menos um;

1.23.6. Restaurar o fragmento [Vue1Fragment]

Os fragmentos são restaurados no método [updateFragment]:


@Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
    }
    // restoration?
    if (fragmentHasToBeInitialized) {
      // restoration condition
      hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
      fragmentHasToBeInitialized = false;
    }
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
    }
    // navigation?
    boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
    if (navigation) {
      // increment visit no
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // the visit number is displayed
      Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
    }
    // change n° current view
    session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
  }
  • Linhas 8–12: Restabelecimento do estado do fragmento. O valor booleano [fragmentHasToBeInitialized] foi inicializado pela classe pai [AbstractFragment]. Quando é verdadeiro, o fragmento acaba de ser reconstruído e deve ser reinicializado. É aqui que isso acontece. Neste exemplo específico, não há nada a fazer. Mostrámos simplesmente que podíamos recuperar o valor da variável booleana [hasBeenVisited] a partir do estado guardado do fragmento (linha 10);
  • linha 11: não se esqueça de definir [fragmentHasToBeInitialized] de volta para false, para que, quando voltarmos a este fragmento mais tarde sem que o dispositivo tenha rodado, não realizemos uma inicialização desnecessária do fragmento;
  • linhas 18–26: incremente o contador de visitas. Aqui, há um desafio: ao restaurar o fragmento, não queremos incrementar este contador. Precisamos de distinguir aqui entre:
    • navegação simples que leva o utilizador de volta ao separador [View 1];
    • uma restauração quando o utilizador roda o seu dispositivo enquanto o separador [View 1] está a ser apresentado;

Distinguimos entre estes dois casos utilizando o número da vista armazenado na sessão. Este número é o da última vista exibida (linha 28).

  • linha 18: ocorre uma navegação em vez de uma atualização se o número da última vista diferir do da vista atual;
  • linhas 21–25: incrementação do contador de visitas e sua exibição;

1.23.7. Restaurar o [PlaceHolderFragment]

Os fragmentos são restaurados no método [updateFragment]:


  // data
  private String text;
  private int numVisit;
  private String newText;
  private boolean hasBeenVisited = false;
  private ObjectMapper jsonMapper = new ObjectMapper();
...
 
public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // which fragment is it?
    int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
    int numView = numSection - 1;
    // does the fragment need to be initialized?
    if (fragmentHasToBeInitialized) {
      // initial text
      text = getString(R.string.section_format, numSection);
      fragmentHasToBeInitialized = false;
    }
    // navigation?
    boolean navigation = session.getCurrentView() != numView;
    if (navigation) {
      // increment visit no
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // modified text
      newText = String.format("%s, visite %s", text, numVisit);
    } else {
      // we are dealing with a restoration
      PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
      newText = state.getText();
    }
    // text display
    textViewInfo.setText(newText);
    // current view
    session.setCurrentView(numView);
}
  • linhas 15-16: determinar 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/recuperação após uma alteração da orientação do dispositivo. Deve ser recuperado aqui. Isto envolve geralmente a recuperação de determinados campos do fragmento;
  • linha 20: o campo [text] na linha 2 deve conter o texto inicial exibido pelo fragmento: [Hello world from section i]. Deve ser regenerado aqui;
  • linha 21: note que o fragmento foi inicializado;
  • linhas 24–36: Tal como no fragmento [Vue1Fragment] anterior, o contador de visitas não deve ser incrementado durante uma restauração. Tal como antes, devemos distinguir entre navegação e restauração;
  • linhas 32–36: caso de restauração;
  • linha 34: o estado do fragmento antes da rotação do dispositivo é recuperado da sessão;
  • linha 35: o texto que estava a ser exibido naquele momento é recuperado;
  • linha 38: este texto é exibido novamente;
  • linha 40: o número da nova vista exibida é registado na sessão;

1.23.8. Gestão de separadores

As secções anteriores não abordaram a gestão de separadores. No entanto, deparámo-nos com um problema no Exemplo 21 ao rodar o dispositivo: apenas o primeiro separador [Vista 1] foi mantido. O segundo separador foi perdido.

Resolvemos este problema na classe [MainActivity] da seguinte forma:


@AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "afterViews");
    }
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
 
 ...
 
    // 1st tab
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // 2nd tab ?
    int numFragment = session.getNumFragment();
    if (numFragment != -1) {
      TabLayout.Tab tab2 = tabLayout.newTab();
      tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
      tabLayout.addTab(tab2);
    }
 
    // which tab to select?
    tabLayout.getTabAt(session.getSelectedTab()).select();
 
...
 
  }
  • linhas 14–16: criação do primeiro separador;
  • linhas 18-23: criação do segundo separador. Para determinar se o devemos criar, verificamos na sessão o número do fragmento apresentado no separador 2. Se este número não for -1 (o seu valor inicial), então o segundo separador é criado. Nesta altura, temos dois separadores, com o primeiro selecionado por predefinição;
  • linha 26: recuperamos da sessão o número da guia que estava selecionada antes do salvar/restaurar e a selecionamos novamente. Se o campo [selectedTab] ainda não tiver sido inicializado pelo código, é utilizado o seu valor inicial de 0;