Skip to content

2. Estrutura de um cliente Android que comunica com um serviço web / jSON

Apresentamos agora um esboço de uma aplicação Android que comunica com um ou mais serviços web / jSON. Trata-se do projeto [client-android-skel], que se encontra na pasta [architecture] dos exemplos:

  

A análise desta aplicação-esqueleto será uma oportunidade para rever alguns pontos que abordámos nos exemplos anteriores. Esta aplicação servirá de estrutura para todas as aplicações futuras. Foi construída após inúmeras iterações. O seu objetivo é factorizar, em classes abstratas, o maior número possível de elementos das aplicações que iremos construir em breve, de modo a evitar ter de escrever sempre o mesmo tipo de código, que se diferencia apenas em pormenores. As suas características são as seguintes:

  • a comunicação assíncrona com o servidor web / jSON é feita através da biblioteca RxJava;
  • o ciclo de vida de um fragmento (update, save, restore) é gerido pela sua classe pai [AbstractFragment], que invoca, em momentos específicos, determinados métodos das suas classes filhas. Assim, a classe filha não tem de se preocupar com as etapas do ciclo de vida, mas apenas com a implementação de determinados métodos impostos pela sua classe pai;
  • o ciclo de vida da atividade (guardar/restaurar) é gerido por uma classe abstrata, [AbstractActivity], que também impõe à atividade filha a implementação de determinados métodos;
  • a classe [AbstractActivity] é capaz de gerir uma aplicação com ou sem separadores, com ou sem imagem de espera, com ou sem autenticação básica junto do servidor web / jSON. A presença ou ausência destes elementos é definida por configuração;

Este esqueleto foi utilizado em todos os exemplos seguintes. Devido à diversidade destes, o que funcionava num exemplo podia não funcionar no exemplo seguinte. Como o esqueleto foi utilizado num total de sete exemplos, ocorreram numerosas iterações. Se o utilizássemos para um oitavo exemplo, é possível que voltássemos a constatar que a especificidade desse novo exemplo geraria novos erros. No entanto, a utilização deste esqueleto simplificará consideravelmente a redação dos exemplos que se seguem. Com efeito, a gestão do ciclo de vida de um fragmento (update, save, restore), aliada ao conceito de adjacência dos fragmentos, é particularmente complexa. Aqui, está totalmente oculta na classe [AbstractFragment].

2.1. Arquitetura do cliente Android

O cliente Android proposto baseia-se na seguinte arquitetura:

  • a camada [DAO] implementa uma interface [IDao]. É esta que comunica com o servidor web / jSON;
  • existe apenas uma atividade que também implementa a interface [IDao]. As vistas recorrem a ela para aceder ao servidor;
  • as vistas são implementadas por fragmentos;

O projeto Android reflete esta arquitetura:

  

Vamos apresentar, um a um, os diferentes elementos deste projeto.

2.2. A configuração do Gradle

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Desde a versão 0.11 do plugin Gradle do Android, é necessário utilizar o android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'

android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }

  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }

  // opções de empacotamento necessárias para conseguir produzir o APK
  packagingOptions {
    exclude 'META-INF/ASL2.0'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/notice.txt'
    exclude 'META-INF/license.txt'
  }
}

def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  apt "org.androidannotations:rest-spring:$AAVersion"
  compile "org.androidannotations:rest-spring-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
  compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
  compile 'io.reactivex:rxandroid:1.2.0'
  compile fileTree(include: ['*.jar'], dir: 'libs')
  testCompile 'junit:junit:4.12'
}

repositories {
  maven {
    url 'https://repo.spring.io/libs-milestone'
  }
}
  • Todos os números de versão estão sujeitos a alterações. No entanto, podemos partir dos números atuais se configurarmos o Android Studio para que estas versões das ferramentas Android (linhas 15-16, 47-48) estejam efetivamente presentes (ver parágrafo 6.11);

2.3. O manifesto da aplicação

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="client.android">

  <uses-permission android:name="android.permission.INTERNET"/>

  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".activity.MainActivity_"
      android:label="@string/app_name"
      android:windowSoftInputMode="stateHidden"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>
  • linha 3: alteraremos o pacote da aplicação;
  • linhas 10, 15: definiremos o valor do item [app_name] no ficheiro [res / values / strings.xml]. Por enquanto, este é o seguinte:

<?xml version="1.0" encoding="utf-8"?>
<resources>

  <!-- nome da aplicação -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

2.4. A organização do código Java

  
  • [architecture] agrupa os principais elementos de organização do código;
  • [activity] contém a única atividade da aplicação;
  • [fragments] agrupa os fragmentos ou vistas da aplicação;
  • [dao] agrupa os elementos de comunicação com o servidor web / jSON;

2.5. Elementos da atividade

 

Image

2.5.1. A vista associada à atividade

A vista [activity_main.xml] associada à atividade é a seguinte:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context=".activity.MainActivity">

  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>

  <!-- contentor de fragmentos -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
  • linha 29: utiliza-se um contentor de fragmentos específico;

A atividade também possui um menu [res / menu / menu_main.xml] para a sua vista:


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

Por enquanto, está vazio. O programador irá preenchê-lo, se necessário.

2.5.2. O contentor de fragmentos [MyPager]

  

package client.android.architecture;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

public class MyPager extends ViewPager {

  // controla o deslizar
  private boolean isSwipeEnabled;
  // controla a rolagem
  private boolean isScrollingEnabled;

  // construtores
  public MyPager(Context context) {
    super(context);
  }

  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  // métodos a redefinir para gerir o deslize
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // deslizar autorizado?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // deslizar autorizado?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

  // controlo da rolagem
  @Override
  public void setCurrentItem(int position){
    super.setCurrentItem(position,isScrollingEnabled);
  }

  // setter
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }

  public void setScrollingEnabled(boolean scrollingEnabled) {
    isScrollingEnabled = scrollingEnabled;
  }
}

Esta classe estende a classe padrão do Android [ViewPager] apenas para gerir o deslize (linha 11) e a rolagem (linha 13) entre vistas.

  • linhas 26-43: os métodos que inibem o deslizar se este tiver sido desativado;
  • linhas 46-49: redefinição do método [setCurrentItem], que serve para alterar a vista exibida. Se a rolagem tiver sido desativada, a mudança de vista será efetuada sem rolagem. Note-se que o programador pode contornar este modo de funcionamento utilizando o método [setCurrentItem(int position, boolean smoothScrolling)], que lhe permite especificar a rolagem que deseja;

2.5.3. A classe [CoreState]

  

A classe [CoreState] é a classe pai dos estados dos diferentes fragmentos:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// tarefa pendente: adicionar aqui as subclasses de [CoreState]
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // fragmento visitado ou não
  protected boolean hasBeenVisited = false;
  // estado do eventual menu do fragmento
  protected MenuItemState[] menuOptionsState;

  // getters e setters
...
}
  • linha 16: cada fragmento tem no seu estado um valor booleano [hasBeenVisited] que indica se já foi visitado ou não. Isto é necessário porque, por vezes, aquando da primeira exibição de um fragmento, há ações específicas a realizar;
  • linha 18: o projeto [client-android-skel] guarda e restaura automaticamente os menus dos fragmentos, caso estes tenham algum. Na tabela MenuItemState[] menuOptionsState, armazena-se o estado de visibilidade de todas as opções do menu;
  • linhas 10-13: tal como foi feito em [Exemple-22], o estado da atividade e dos seus fragmentos será guardado na sessão, que, por sua vez, será guardada sob a forma de uma cadeia jSON. Veremos que a sessão memoriza um tabuleiro de elementos do tipo [CoreState]. Se não fizermos nada, será então a cadeia jSON, de um tipo [CoreState], que será guardada. No entanto, pretendemos guardar os estados dos fragmentos, ou seja, os estados derivados de [CoreState]. Para que seja gerada a cadeia jSON do tipo derivado e não a do tipo pai, é necessário declarar os tipos derivados conforme indicado nas linhas 10 a 13. A classe [CoreState] é uma das classes da arquitetura que o programador deve modificar para cada nova aplicação (linhas 10-13);

2.5.4. A interface [IMainActivity]

  

A interface [IMainActivity] define o que os fragmentos podem solicitar à atividade na seguinte arquitetura:

Image


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // acesso à sessão
  ISession getSession();

  // mudança de vista
  void navigateToView(int position, ISession.Action action);

  // gestão da espera
  void beginWaiting();

  void cancelWaiting();

  // constantes da aplicação (a modificar) -------------------------------------

  // modo de depuração
  boolean IS_DEBUG_ENABLED = true;

  // tempo máximo de espera pela resposta do servidor
  int TIMEOUT = 1000;

  // tempo de espera antes da execução do pedido do cliente
  int DELAY = 0;

  // autenticação básica
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adjacência dos fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra de separadores
  boolean ARE_TABS_NEEDED = false;

  // imagem de espera
  boolean IS_WAITING_ICON_NEEDED = false;

  // número de fragmentos da aplicação
  int FRAGMENTS_COUNT = 0;

  // tarefa: adicione aqui as suas constantes e outros métodos
}
  • linha 6: a interface [IMainActivity] estende a interface [IDao] da camada [DAO];
  • linha 9: é a atividade que dá acesso à sessão sob a forma de uma instância da interface [ISession];
  • linha 12: é através desta atividade que se muda de vista. O segundo parâmetro é a ação que provoca essa mudança de vista, um dos valores SUBMIT, NAVIGATION, RESTORE;
  • linhas 15-17: é a atividade que gere a imagem de espera;
  • linha 22: para a depuração da aplicação;
  • linha 25: para não esperar demasiado tempo caso o servidor deixe de responder;
  • linha 28: durante a depuração, definiremos um valor de alguns segundos para termos tempo de cancelar a operação com o servidor e ver o que acontece;
  • linha 31: para true se o serviço jSON solicitar uma autenticação básica;
  • linha 34: adjacência de fragmentos;
  • linha 37: para vrai se a aplicação tiver separadores;
  • linha 39: para vrai se a aplicação comunicar com um servidor web / jSON e se se pretender mostrar uma imagem de espera durante as trocas de dados;
  • linha 43: o número de fragmentos geridos pela aplicação;

A interface [IMainActivity] é o segundo elemento da arquitetura que o programador deve preencher (linha 45).

2.5.5. A interface [IDao]

A interface [IMainActivity] estende a seguinte interface [IDao]:

  

package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL do serviço web
  void setUrlServiceWebJson(String url);

  // utilizador
  void setUser(String user, String mdp);

  // tempo limite do cliente
  void setTimeout(int timeout);

  // autenticação básica
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // modo de depuração
  void setDebugMode(boolean isDebugEnabled);

  // tempo de espera do cliente, em milissegundos, antes da solicitação
  void setDelay(int delay);

  // tarefa pendente: declare aqui a sua interface
}
  • linha 24: o programador irá completar a interface aqui;

2.5.6. A sessão

  

A classe [Session] encapsula os elementos partilhados pela atividade e pelos fragmentos. Ela implementa a seguinte interface [ISession]:


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;

public interface ISession {

  // número da última vista apresentada
  int getPreviousView();

  void setPreviousView(int numView);

  // último estado de uma vista
  CoreState getCoreState(int numView);

  void setCoreState(int numView, CoreState coreState);

  // ação em curso
  enum Action {
    SUBMIT, NAVIGATION, RESTORE, NONE
  }

  Action getAction();

  void setAction(Action action);

  // estados de todas as vistas -
  // não é utilizado pelo código, mas é necessário para a serialização/deserialização jSON
  CoreState[] getCoreStates();

  void setCoreStates(CoreState[] coreStates);

  // n.º do último separador selecionado
  int getPreviousTab();

  void setPreviousTab(int position);

  // navegação ao selecionar um separador
  boolean isNavigationOnTabSelectionNeeded();

  void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}

Introduzimos a interface [ISession] para impor a presença de determinados métodos na sessão:

  • linhas 7-10: o número da última vista (fragmento) apresentada;
  • linhas 12-15: o estado de uma vista específica;
  • linhas 17-24: introduzimos o conceito de ação em curso. Existem quatro (linha 17):
    • RESTORE: está em curso um processo de gravação/restauração. Não há mudança de vista;
    • NAVIGATION: está em curso uma navegação. Denominaremos aqui «navegação» uma mudança de vista em que a nova vista pode ser restaurada a partir do seu último estado guardado na sessão;
    • SUBMIT: atribuir-se-á o tipo [SUBMIT] a uma ação em curso, quando houver uma mudança de vista e a nova vista depender do estado da atividade em geral e não apenas do seu próprio estado. Por vezes, é difícil distinguir entre NAVIGATION e SUBMIT. Nesse caso, considera-se o caso mais geral, o SUBMIT;
    • NONE: valor da ação quando esta ainda não recebeu o seu primeiro valor;
  • linhas 26-30: os estados da atividade e dos fragmentos serão armazenados numa matriz do tipo CoreState[]. Para que esta seja gerida corretamente durante as serializações/deserializações jSON, é necessário que possua um getter e um setter;
  • linhas 32-35: número do último separador selecionado. É utilizado durante o ciclo de gravação/restauração para voltar a selecionar o separador que estava selecionado antes da rotação do dispositivo;
  • linhas 37-40: gestão de um valor booleano que indica se a seleção de um separador deve ser acompanhada por uma mudança de fragmento;

A interface [ISession] é implementada pela seguinte classe abstrata [AbstractSession]:


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import com.fasterxml.jackson.annotation.JsonIgnore;

public class AbstractSession implements ISession {
  // n.º da vista anterior
  private int preViousView;

  // estado das vistas
  private CoreState[] coreStates = new CoreState[0];

  // ação em curso
  private Action action = Action.NONE;

  // separador selecionado anteriormente
  private int previousTab;

  // navegação ao selecionar o separador
  @JsonIgnore
  private boolean navigationOnTabSelectionNeeded = true;

  // construtor
  public AbstractSession() {
    // inicialização da tabela de estados dos fragmentos
    coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
    for (int i = 0; i < coreStates.length; i++) {
      coreStates[i] = new CoreState();
    }
  }


  // interface ISession ---------------------------------------------------------
  @Override
  public int getPreviousView() {
    return preViousView;
  }

  @Override
  public void setPreviousView(int numView) {
    this.preViousView = numView;
  }

  @Override
  public CoreState getCoreState(int numView) {
    return coreStates[numView];
  }

  @Override
  public void setCoreState(int numView, CoreState coreState) {
    coreStates[numView] = coreState;
  }

  @Override
  public Action getAction() {
    return action;
  }

  @Override
  public void setAction(Action action) {
    this.action = action;
  }

  @Override
  public CoreState[] getCoreStates() {
    return coreStates;
  }

  @Override
  public void setCoreStates(CoreState[] coreStates) {
    this.coreStates = coreStates;
  }

  @Override
  public int getPreviousTab() {
    return previousTab;
  }

  @Override
  public void setPreviousTab(int position) {
    this.previousTab = position;
  }

  @Override
  public boolean isNavigationOnTabSelectionNeeded() {
    return navigationOnTabSelectionNeeded;
  }

  @Override
  public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
    this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
  }
}
  • linha 9: o número da vista que estava a ser apresentada antes da que está atualmente a ser apresentada. Esta informação é útil quando é possível aceder a uma vista a partir de vários locais. É tipicamente o caso na navegação por separadores. A vista apresentada pode, assim, saber qual era a vista anterior;
  • linha 12: a tabela de estados de todos os fragmentos exibidos pela atividade;
  • linha 18: o número da aba selecionada anteriormente. Desempenha um papel semelhante ao do número da vista anterior da linha 9. Esta informação é útil quando ocorre uma rotação do dispositivo e é necessário voltar à aba que estava selecionada antes da rotação;
  • linha 22: um valor booleano que indica se a seleção de uma aba deve ser acompanhada por uma alteração do fragmento exibido. É importante saber que o projeto [client-android-skel] faz uma gestão separada das abas e dos fragmentos, para poder ser utilizado em casos em que o número de abas é inferior ao número de fragmentos. Existem dois tipos de seleção:
    • uma seleção efetuada pelo utilizador ao clicar numa separador. Neste caso, geralmente o fragmento exibido deve mudar;
    • uma seleção por software através do método [Tablayout.Tab.select()]. Neste caso, a alteração do fragmento exibido nem sempre é desejável. Eis dois exemplos:
      • durante uma rotação do dispositivo, a atividade é recriada, assim como as separadores. No entanto, quando a primeira separador é criada, esta é automaticamente submetida a uma operação de software [select]. Nessa altura, não é desejável alterar o fragmento exibido, uma vez que nos encontramos numa fase de recriação da atividade em que o fragmento que acabará por ser exibido não será necessariamente aquele associado ao primeiro separador;
      • uma vez que a gestão dos separadores é independente da dos fragmentos, pode ser necessário atualizar os separadores (eliminação, adição) sem interferir com os fragmentos a eles associados. No entanto, algumas destas operações podem, mais uma vez, desencadear uma operação de software [select] implícita numa das separadores. Esta seleção não tem, portanto, de se traduzir necessariamente numa navegação para o fragmento associado;
  • linha 21: o campo [navigationOnTabSelectionNeeded] não se destina a ser guardado durante as operações de gravação da atividade e dos seus fragmentos. A anotação [@JsonIgnore] faz com que o campo seja ignorado durante as serializações/deserializações jSON;
  • linhas 25-31: o construtor inicializa a matriz de estados dos fragmentos [FRAGMENTS_COUNT] da aplicação. Os elementos desta matriz são inicializados com o campo [hasBeeenVisited=false]. Esta informação é utilizada para determinar se se trata ou não da primeira visita ao fragmento;

A classe [Session] é a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // dados a partilhar entre os próprios fragmentos e entre fragmentos e atividades
  // os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
  // não se esqueça dos getters e setters necessários para a serialização/deserialização em jSON
}
  • linha 5: a classe [Session] estende a classe [AbstractSession] que acabámos de ver. O programador irá colocar aqui os elementos a partilhar entre os próprios fragmentos e entre os fragmentos e a atividade. Note-se que a classe [Session] já não está anotada pela anotação AA [@EBean]. Tornou-se uma classe normal;

2.5.7. A classe abstrata [AbstractActivity]

  

2.5.7.1. Squelette

A classe [AbstractActivity] é uma classe com mais de 300 linhas. Vamos analisá-la por etapas. A sua estrutura básica é a seguinte:


package client.android.architecture;

import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import client.android.R;
import client.android.dao.service.IDao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // camada [DAO]
  private IDao dao;
  // a sessão
  protected Session session;

  // o contentor de fragmentos
  protected MyPager mViewPager;
  // a barra de ferramentas
  private Toolbar toolbar;
  // a imagem de espera
  private ProgressBar loadingPanel;
  // barra de separadores
  protected TabLayout tabLayout;

  // o gestor de fragmentos ou secções
  private FragmentPagerAdapter mSectionsPagerAdapter;
  // nome da classe
  protected String className;
  // mapeador jSON
  private ObjectMapper jsonMapper;

  // construtor
  public AbstractActivity() {
    // nome da classe
    className = getClass().getSimpleName();
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "constructeur");
    }
    // jsonMapper
    jsonMapper = new ObjectMapper();
  }

  // implementação IMainActivity --------------------------------------------------------------------
  ...

  // ciclo de vida - gravação/restauração da atividade ------------------------------------
  ...

  // gestão da imagem de espera ---------------------------------
  ...

  // interface IDao -----------------------------------------------------
  ...

  // o gestor de fragmentos --------------------------------
  ...

  // classes filhas
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

  protected abstract int getFirstView();

}

A classe [AbstractActivity]:

  • implementa a interface [IMainActivity] (linhas 21, 55);
  • gere o armazenamento e a restauração da atividade e dos seus fragmentos durante uma rotação do dispositivo (linha 58);
  • gere a imagem de espera durante uma troca de dados com o servidor web / jSON (linha 61);
  • implementa a interface IDao da camada [DAO] (linha 64);
  • implementa o gestor de fragmentos (linha 67);
  • exige que as suas classes filhas incluam seis métodos (linhas 71-81);

2.5.7.2. Implementar a interface [IMainActivity]

A implementação da interface [IMainActivity] (ver parágrafo 2.5.4) é a seguinte:


  // implementação IMainActivity --------------------------------------------------------------------
  @Override
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position, ISession.Action action) {
    if (IS_DEBUG_ENABLED) {
      Log.d(className, String.format("navigation vers vue %s sur action %s", position, action));
    }
    // exibição de novo fragmento
    mViewPager.setCurrentItem(position);
    // regista-se a ação em curso durante esta mudança de vista
    session.setAction(action);
}

2.5.7.3. Gravação do estado da atividade e dos seus fragmentos

O estado da atividade e dos seus fragmentos encontra-se inteiramente na sessão. Trata-se, portanto, de guardar essa sessão. Retomamos aqui o que foi feito no projeto [Exemple-22] (ver parágrafo 1.23):


  // gestão do armazenamento e restauração da atividade ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // pai
    super.onSaveInstanceState(outState);
    // guardar sessão sob a forma de uma cadeia jSON
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // registo
    if (IS_DEBUG_ENABLED) {
      try {
        Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}

2.5.7.4. Restauração do estado da atividade e dos seus fragmentos

Trata-se de restaurar a sessão. Procedemos tal como foi demonstrado em [Exemple-22]:


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // pai
    super.onCreate(savedInstanceState);
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    // alguma coisa para restaurar?
    if (savedInstanceState != null) {
      // recuperação de sessão
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // registo
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    } else {
      // sessão
      session = new Session();
    }
...
  • linhas 10-26: se o parâmetro [Bundle savedInstanceState] da linha 2 não for null, então a sessão é restaurada (linhas 12-17);
  • linhas 26-29: o caso em que o parâmetro [Bundle savedInstanceState] da linha 2 é null corresponde ao primeiro arranque da atividade. É então criada uma sessão vazia;

2.5.7.5. Inicialização da camada [DAO]


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // pai
    super.onCreate(savedInstanceState);
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    ...
    // camada [DAO]
    dao = getDao();
    if (dao != null) {
      // configuração da camada [DAO]
      setDebugMode(IS_DEBUG_ENABLED);
      setTimeout(TIMEOUT);
      setDelay(DELAY);
      setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
    }
...
  // classes filhas
  protected abstract IDao getDao();
....
}
  • linha 11: é solicitada à atividade filha (linha 21) uma referência à camada [DAO];
  • linhas 14-17: se a camada [DAO] existir, esta é configurada a partir das informações contidas na interface [IMainActivity];

2.5.7.6. Inicialização da vista associada à atividade

A vista associada à atividade foi apresentada no parágrafo 2.5.1:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context=".activity.MainActivity">

  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>

  <!-- recipiente de fragmentos -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>

Esta vista é inicializada com o seguinte código:


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // pai
    super.onCreate(savedInstanceState);
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // vista associada
    setContentView(R.layout.activity_main);
    // componentes da vista ---------------------
    // barra de ferramentas
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // imagem de espera?
    if (IS_WAITING_ICON_NEEDED) {
      // adiciona-se a imagem de espera
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding loadingPanel");
      }
      // criação de ProgressBar
      loadingPanel = new ProgressBar(this);
      loadingPanel.setVisibility(View.INVISIBLE);
      // adição do ProgressBar à barra de ferramentas
      toolbar.addView(loadingPanel);
    }
...
  • linha 11: a vista XML [activity_main] está associada à atividade;
  • linhas 14-15: a barra de ferramentas está integrada e é suportada;
  • linhas 17-27: eventual adição de uma imagem de espera: se o valor booleano [IS_WAITING_ICON_NEEDED] for verdadeiro na interface [IMainActivity];
  • linha 23: criação da imagem de espera do tipo [ProgressBar], referenciada pelo campo [loadingPanel];
  • linha 24: inicialmente, esta imagem está oculta;
  • linha 26: é adicionada à barra de ferramentas;

2.5.7.7. Gestão de separadores

A interface [IMainActivity] pode requerer uma barra de separadores. Esta é adicionada e gerida da seguinte forma:


// barra de separadores
  protected TabLayout tabLayout;
...

    // barra de separadores?
    if (ARE_TABS_NEEDED) {
      // adiciona-se a barra de separadores
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding tablayout");
      }
      // sem navegação por seleção até à exibição de um fragmento
      session.setNavigationOnTabSelectionNeeded(false);
      // criação da barra de separadores
      tabLayout = new CustomTabLayout(this);
      tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
      // adição da barra de separadores à barra de aplicações
      AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
      appBarLayout.addView(tabLayout);
      // gestor de eventos da barra de separadores
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
          // foi selecionado um separador
          if (IS_DEBUG_ENABLED) {
            Log.d(className, String.format("onTabSelected n° %s, action=%s, tabCount=%s isNavigationOnTabSelectionNeeded=%s",
              tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
          }
          if (session.isNavigationOnTabSelectionNeeded()) {
            // posição da aba
            int position = tab.getPosition();
            // memória
            session.setPreviousTab(position);
            // exibição do fragmento associado?
            navigateOnTabSelected(position);
          }
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {

        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {

        }
      });
    }

...
  // classes filhas
  protected abstract void navigateOnTabSelected(int position);
...
  • linhas 12-48: adição e gestão de uma barra de separadores;
  • linha 6: a barra de separadores é adicionada se a constante [ARE_TABS_NEEDED] estiver definida como vrai na interface [IMainActivity];
  • linha 12: durante a criação da barra de separadores, podem ocorrer operações [Tablayout.Tab.select] implícitas (não são provocadas pelo utilizador). Define-se o valor booleano [session.navigationOnTabSelectionNeeded] como faux para evitar qualquer navegação durante estas seleções falsas. Caberá ao programador selecionar o fragmento a apresentar com o método [navigateToView]. A variável booleana [session.navigationOnTabSelectionNeeded] será reposto para vrai quando este fragmento for exibido (ver classe AbstractFragment);
  • linha 14: criação de uma barra de separadores referenciada pelo campo [tabLayout]. Utilizamos uma barra de separadores personalizada [CustomTabLayout], à qual voltaremos mais tarde;
  • linha 15: definimos as cores dos títulos dos separadores. Estas encontram-se no seguinte ficheiro [res / color / tab_txt.xml]:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_selected="true" android:color="#FFFF00" />
  <item android:state_selected="false" android:color="#FFFFFF" />
</selector>
    • linha (c): a cor do título da aba quando esta está selecionada;
    • linha (d): a cor do título do separador quando este não está selecionado;

Este ficheiro é, naturalmente, editável. Os códigos hexadecimais das cores podem ser consultados, por exemplo, aqui.

  • linhas 17-18: adição desta barra de separadores à barra de aplicações presente na vista XML [activity_main];
  • linhas 20-47: gestor de eventos da barra de separadores;
  • linhas 22-36: apenas o evento [onTabSelected] é gerido. Corresponde a um clique na aba [Tab tab] passada como parâmetro para o método ou a uma operação de software [TabLayout.Tab.select];
  • linha 30: posição do separador selecionado;
  • linha 32: esta posição é guardada durante a sessão;
  • linha 34: trata-se agora de apresentar o fragmento associado a esta guia. Apenas a classe filha (linha 52) pode efetuar esta associação. Note-se que não se associa a barra de guias ao contentor de fragmentos [mViewPager], tal como foi feito em alguns exemplos analisados. Aqui, separa-se totalmente a gestão da barra de separadores da gestão dos fragmentos. É por isso que, quando se clica num separador, é necessário indicar qual a vista que se pretende ver apresentada;
  • linha 28: distingue-se a seleção de separador com ou sem navegação. Em geral, quando o utilizador clica numa aba, pretende-se que haja navegação, enquanto que, numa seleção por software, não se pretende que haja. É o programador que distingue estes dois casos com o elemento [session.navigationOnTabSelectionNeeded]. Quando a navegação não é efetuada, o número da última aba selecionada não é registado na sessão. Caberá ao programador fazê-lo;

2.5.7.8. O gestor de separadores [CustomTabLayout]

  

Utilizamos um gestor de separadores personalizado para poder apresentar o título dos separadores com diferentes tipos de letra. A classe [CustomTabLayout] é a seguinte:


package client.android.architecture.custom;

import android.content.Context;
import android.graphics.Typeface;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class CustomTabLayout extends TabLayout {
  private Typeface mTypeface;

  public CustomTabLayout(Context context) {
    super(context);
    init();
  }

  public CustomTabLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
  }

  public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
  }

  private void init() {
    mTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/Roboto-Bold.ttf");
  }

  @Override
  public void addTab(Tab tab) {
    super.addTab(tab);

    ViewGroup mainView = (ViewGroup) getChildAt(0);
    ViewGroup tabView = (ViewGroup) mainView.getChildAt(tab.getPosition());

    int tabChildCount = tabView.getChildCount();
    for (int i = 0; i < tabChildCount; i++) {
      View tabViewChild = tabView.getChildAt(i);
      if (tabViewChild instanceof TextView) {
        ((TextView) tabViewChild).setTypeface(mTypeface, Typeface.NORMAL);
      }
    }
  }

}
  • a personalização do tipo de letra dos títulos das separadores é feita nas linhas 30 e 44;

O ficheiro [fonts] é o seguinte:

  

Fontes:

  • o código da classe [CustomTabLayout] foi encontrado em URL e [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout];
  • as fontes foram encontradas em URL e [https://www.fontsquirrel.com/fonts/roboto];

2.5.7.9. Últimas inicializações


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // pai
    super.onCreate(savedInstanceState);
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // instanciação do gestor de fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // o contentor de fragmentos está associado ao gestor de fragmentos
    // ou seja, o fragmento n.º i do contentor de fragmentos é o fragmento n.º i fornecido pelo gestor de fragmentos
    mViewPager = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // inibe-se o deslize entre fragmentos
    mViewPager.setSwipeEnabled(false);
    // adjacência dos fragmentos
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    // é apresentada a primeira vista
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    // passa-se o controlo para a atividade filha
    onCreateActivity();
  }
...
  // classes filhas
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • linhas 10-19: aqui encontramos código frequentemente presente nos exemplos analisados;
  • linhas 21-23: exibição da primeira vista. Existem, sem dúvida, várias formas de distinguir este caso. Aqui, utilizámos o facto de, para a primeira vista, o valor da ação que provoca a mudança de vista ser NONE;
  • linha 22: não fazemos qualquer suposição sobre o primeiro fragmento a apresentar. Nos nossos exemplos, este foi frequentemente o fragmento n.º 0, mas nem sempre (ver Exemplo-22). Por isso, pediremos à atividade filha (linha 30) que nos indique qual é essa primeira vista;
  • linha 25: aqui, fatorizámos tudo o que era possível. Agora, a classe filha tem as suas próprias inicializações a efetuar (linha 29);

2.5.7.10. Gestão da imagem de espera

Na classe [AbstractActivity], a imagem de espera é gerida pelos dois métodos seguintes:


  // gestão da imagem de espera ---------------------------------
  public void cancelWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.INVISIBLE);
    }
  }

  public void beginWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.VISIBLE);
    }
}

2.5.7.11. Implementação da interface [IDao]

Na classe [AbstractActivity], a interface [IDao] (ver parágrafo 2.5.5) é implementada da seguinte forma:


public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // camada [DAO]
  private IDao dao;
...
  // interface IDao -----------------------------------------------------
  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    dao.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    dao.setTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    dao.setBasicAuthentification(isBasicAuthentificationNeeded);
  }

  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    dao.setDebugMode(isDebugEnabled);
  }

  @Override
  public void setDelay(int delay) {
    dao.setDelay(delay);
}
  • linha 3: recorde-se que o valor deste campo foi fornecido pela atividade filha no método [onCreate];

2.5.7.12. Implementação do gestor de fragmentos

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


...
  // o gestor de fragmentos --------------------------------
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    private AbstractFragment[] fragments;

    // construtor
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
      // fragmentos da classe filha
      fragments = getFragments();
    }

    // deve apresentar o fragmento na posição n.º
    @Override
    public AbstractFragment getItem(int position) {
      // renderiza-se o fragmento
      return fragments[position];
    }

    // indica o número de fragmentos a gerir
    @Override
    public int getCount() {
      return fragments.length;
    }

    // apresenta o título do fragmento na posição n.º
    @Override
    public CharSequence getPageTitle(int position) {
      return getFragmentTitle(position);
    }
  }

  // classes filhas
  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • linha 5: o array dos fragmentos associados à atividade. Todos os fragmentos serão derivados da classe [AbstractFragment];
  • linhas 8-12: trata-se do construtor que inicializa a matriz de fragmentos. Este solicita os fragmentos à classe filha da atividade (linha 35);
  • linhas 28-31: os títulos dos fragmentos podem ser utilizados numa aplicação em que haja tantos separadores quantos os fragmentos. Neste caso, é possível atribuir ao separador o título do fragmento. Aqui, esses títulos são solicitados à classe filha (linha 37);

2.5.7.13. O método [onResume]

O método [onResume] é executado pouco antes de a vista associada à atividade se tornar visível. É utilizado aqui para selecionar um separador após um processo de gravação/restauração:


  @Override
  public void onResume() {
    // pai
    super.onResume();
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onResume");
    }
    // se for uma restauração, então é necessário restaurar o último separador selecionado
    if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
      tabLayout.getTabAt(session.getPreviousTab()).select();
    }
}
  • linha 10: seleção do separador que estava selecionado antes do processo de gravação/restauração. É importante lembrar que, no método [onCreate] — que, no ciclo de vida da atividade, é executado antes do método [onResume] —, a navegação ao selecionar um separador foi desativada. Portanto, neste caso, há seleção de um separador, mas não há mudança de fragmento;

2.5.7.14. Résumé

A classe abstrata [AbstractActivity] será a classe pai da única atividade da aplicação.

A atividade filha deverá implementar os seis métodos seguintes:


  // classes filhas
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

protected abstract int getFirstView();

Além disso, a atividade filha tem acesso aos seguintes elementos protegidos da sua classe pai:


  // a sessão
  protected ISession session;
  // o contentor dos fragmentos
  protected MyPager mViewPager;
  // barra de separadores
  protected CustomTabLayout tabLayout;
  // nome da classe
protected String className;

2.5.8. A atividade [MainActivity]

  

A classe [MainActivity] pode ter um nome diferente. A sua única restrição é implementar a interface [IMainActivity]. A classe fornecida por predefinição é a seguinte:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // camada [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessão
  private Session session;

  // métodos da classe pai -----------------------
  @Override
  protected void onCreateActivity() {
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sessão
    this.session = (Session) super.session;
    // tarefa pendente: continuamos as inicializações iniciadas pela classe pai
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // tarefa pendente: definir os fragmentos aqui
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // a fazer: definir aqui os títulos dos fragmentos
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // tarefa: navegação por separadores — definir a vista a apresentar
  }

  @Override
  protected int getFirstView() {
    // tarefa: navegação por separadores — definir a primeira vista a apresentar
    return 0;
  }
}
  • linha 14: para que a notação AA [@Bean] da linha 19 seja compreendida, é necessário que a atividade tenha a notação AA [@EActivity];
  • linha 15: a atividade está associada ao menu XML [menu_main]. Atualmente, este menu está vazio. O programador terá de o preencher, caso seja necessário;
  • linha 16: a classe estende a classe [AbstractActivity];
  • linhas 19-20: uma referência à camada [DAO]. Esta será instanciada pela biblioteca AA antes de este campo ser inicializado. Isto implica que o bean AA [Dao] deve existir. É sempre esse o caso com a aplicação-esqueleto que fornecemos. Mesmo numa aplicação sem a camada [DAO], pode-se deixar o pacote [dao] existir. Isso não acarreta complicações;
  • linha 22: a sessão como instância do tipo [Session]. A sessão existe na classe pai [AbstractActivity], mas como instância da interface [ISession] (linha 32);
  • linhas 24-63: os seis métodos impostos pela classe pai [AbstractActivity];
  • linhas 36-39: o método [getDao] devolve uma referência à camada [DAO]. Aqui, essa referência nunca é null. No entanto, na classe pai [AbstractActivity], previu-se o caso em que a classe filha devolvesse uma referência null para indicar que não existia a camada [DAO]. Se se quiser utilizar esta possibilidade (que, na minha opinião, não é muito útil), é aqui que se deve atribuir o ponteiro null;

2.6. A camada [DAO]

Image

  

2.6.1. A interface IDao

Foi apresentada no parágrafo 2.5.5:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL do serviço web
  void setUrlServiceWebJson(String url);

  // utilizador
  void setUser(String user, String mdp);

  // tempo limite do cliente
  void setTimeout(int timeout);

  // autenticação básica
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // modo de depuração
  void setDebugMode(boolean isDebugEnabled);

  // tempo de espera do cliente, em milissegundos, antes da solicitação
  void setDelay(int delay);

  // tarefa pendente: declare aqui a sua interface
}

O programador irá adicionar os métodos da sua camada [DAO] a partir da linha 24.

2.6.2. A interface [WebClient]

  

A interface [WebClient] é a seguinte:


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

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

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // tarefa pendente: declare aqui os URL a atingir
}

O programador irá adicionar os métodos que comunicam com o URL, expostos pelo servidor jSON, a partir da linha 17.

2.6.3. O interceptor de autenticação [MyAuthInterceptor]

  

A classe [MyAuthInterceptor] é a seguinte:


package client.android.dao.service;

import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;

@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {

  // utilizador
  private String user;
  // palavra-passe
  private String mdp;

  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // cabeçalhos HTTP da solicitação HTTP interceptada
    HttpHeaders headers = request.getHeaders();
    // o cabeçalho HTTP de autenticação básica
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    // adição aos cabeçalhos HTTP
    headers.setAuthorization(auth);
    // prossegue-se o ciclo de vida da solicitação HTTP
    return execution.execute(request, body);
  }

  // elementos da autenticação
  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}

Esta classe gera o cabeçalho de autenticação HTTP a seguir:

Authorization: Basic code

onde [code] é o código Base64 da cadeia «user:mp». Esta classe só é utilizada se o servidor jSON esperar este tipo de autenticação. Existem outras.

Nota: a utilização desta classe é ilustrada no parágrafo 3.6.3.1.

2.6.4. A classe [AbstractDao]

  

A classe [AbstractDao] é a seguinte:


package client.android.dao.service;

import android.util.Log;
import client.android.architecture.core.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;

public abstract class AbstractDao {

  // mapeador jSON
  private ObjectMapper mapper = new ObjectMapper();
  // modo de depuração
  protected boolean isDebugEnabled;
  // nome da classe
  protected String className;
  // tempo de espera antes da execução da consulta
  private int delay;

  // construtor
  public AbstractDao() {
    // nome da classe
    className = getClass().getName();
    Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
  }

  // métodos protegidos ----------------------------------------------------------
  // interface genérica
  protected interface IRequest<T> {
    T getResponse();
  }

  // solicitação genérica a um serviço web / jSON
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // registo
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("delay=%s", delay));
    }
    // execução do serviço — aguarda-se uma única resposta
    return Observable.create(new Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // execução do serviço
        try {
          // em espera?
          if (delay > 0) {
            Thread.sleep(delay);
          }
          // a execução da consulta síncrona está a decorrer
          T response = request.getResponse();
          // registo
          if (isDebugEnabled) {
            String log;
            if (response instanceof String) {
              log = (String) response;
            } else {
              log = mapper.writeValueAsString(response);
            }
            Log.d(className, String.format("response=%s sur thread [%s]", log, Thread.currentThread().getName()));
          }
          // envia-se a resposta ao observador
          subscriber.onNext(response);
          // a indicação do fim do observável
          subscriber.onCompleted();
        } catch (InterruptedException | JsonProcessingException | RuntimeException e) {
          // registo
          if (isDebugEnabled) {
            try {
              Log.d(className, String.format("Thread [%s], Exception communication avec serveur : %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
            } catch (JsonProcessingException e1) {
              Log.d(className, String.format("Erreur jSON imprévue"));
            }
          }
          // lança-se uma exceção
          subscriber.onError(new DaoException(e, 100));
        }
      }
    });
  }

  // modo de depuração
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
  }

  public void setDelay(int delay) {
    this.delay = delay;
  }
}
  • linhas 35-81: o método [getResponse] utiliza a biblioteca RxAndroid para renderizar um tipo [Observable<T>]. Ao contrário de alguns exemplos vistos anteriormente, não se renderiza um tipo [Response<T>], que é um tipo proprietário, mas sim um tipo T qualquer;
  • linha 35: o método [getResponse] recebe como parâmetro uma instância do tipo [IRequest<T>] das linhas 30-32, cujo método [IRequest.getReponse()] obtém o tipo T através de uma operação HTTP síncrona;
  • linhas 48-50: artificialmente, aguarda-se [delay] milissegundos. Em produção, utilizar-se-á [delay=0]. Na fase de depuração, utilizar-se-á [delay=qqs secondes] para dar ao utilizador a oportunidade de cancelar a operação assíncrona e, assim, ver como o código se comporta nessa situação;
  • linha 52: a resposta esperada é solicitada através de uma requisição síncrona;
  • linha 64: assim que a resposta é recebida, é passada para o observador;
  • linha 66: indica-se que não haverá mais emissões. Estamos aqui no caso específico de uma ação assíncrona que devolve apenas um elemento;
  • linhas 67-78: em caso de exceção, a exceção é emitida para o observador (linha 77);

2.6.5. A classe [Dao]

  

A classe [Dao] é a seguinte:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // cliente do serviço web
  @RestService
  protected WebClient webClient;
  // segurança
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // o RestTemplate
  private RestTemplate restTemplate;
  // fábrica do RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // registo
    Log.d(className, "afterInject");
    // construímos o restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // fixa-se o conversor jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // define-se o restTemplate do cliente web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // define-se o URL do serviço web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // regista-se o utilizador no interceptor
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // configuração de fábrica
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // interceptor de autenticação?
    if (isBasicAuthentificationNeeded) {
      // adiciona-se o interceptor de autenticação
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // métodos privados -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // tarefa pendente: implementação IDao
}
  • linhas 21-22: injeção do bean AA [WebClient], que irá assegurar as comunicações com o servidor web / jSON;
  • linhas 24-25: injeção do interceptor de autenticação;
  • linhas 31-42: método executado após a inserção dos campos das linhas 21-25;
  • linha 37: o objeto [RestTemplate], que assegura as trocas cliente/servidor, é criado a partir de um factory. Não é imprescindível, mas é através do factory que se podem configurar os tempos de espera das comunicações. É por isso que não utilizamos o construtor sem parâmetros [RestTemplate()];
  • linha 39: adicionamos um conversor jSON aos conversores do [RestTemplate]. Este será o único conversor. Assim, quando um método do cliente [WebClient] receber uma cadeia jSON do servidor, esta será automaticamente deserializada no objeto que o método deve devolver;
  • linha 41: o objeto [RestTemplate] assim configurado é passado para o cliente web, que irá assegurar as trocas cliente/servidor através dele;
  • linhas 44-48: define-se o URL como raiz do servidor web / jSON. Todas as URL declaradas na classe [WebClient] são URL relativas a esta URL raiz;
  • linhas 50-54: este método permite especificar o proprietário da ligação quando esta é controlada por uma autorização de tipo básico (ver parágrafo 2.6.3);
  • linhas 56-64: definem os timeouts das trocas cliente/servidor. Isto é feito através do factory do objeto [RestTemplate] que rege as trocas;
  • linhas 66-78: este método permite indicar que o servidor é protegido por uma autenticação do tipo básico;
  • linhas 72-77: se for solicitada uma autenticação do tipo básico, o interceptor de autenticação inserido na linha 25 é adicionado aos interceptores do objeto [RestTemplate]. Este interceptor irá adicionar automaticamente a todas as solicitações do cliente web a linha HTTP de autenticação básica esperada pelo servidor;
  • o programador implementará a interface [IDao] a partir da linha 87;

2.7. Os fragmentos

  

2.7.1. A classe [MenuItemState]

A classe [MenuItemState] encapsula o estado de uma opção de menu:


package client.android.architecture;

public class MenuItemState {

  // identificador da opção do menu
  private int menuItemId;
  // visibilidade da opção
  private boolean isVisible;

  // construtores
  public MenuItemState() {

  }

  public MenuItemState(int menuItemId, boolean isVisible) {
    this.menuItemId = menuItemId;
    this.isVisible = isVisible;
  }

  // getters e setters
...
}

2.7.2. A classe [Utils]

A classe [Utils] reúne métodos estáticos utilitários:


package client.android.architecture;

import java.util.ArrayList;
import java.util.List;

public class Utils {

  // lista de mensagens de uma exceção - versão 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // cria-se uma lista com as mensagens de erro da pilha de exceções
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }

  // lista de mensagens de uma exceção - versão 2
  static public String getMessageForAlert(Throwable th) {
    // construção do texto a apresentar
    StringBuilder texte = new StringBuilder();
    List<String> messages = getMessagesFromException(th);
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // resultado
    return texte.toString();
  }

  // lista de mensagens de uma exceção - versão 3
  static public String getMessageForAlert(List<String> messages) {
    // construção do texto a apresentar
    StringBuilder texte = new StringBuilder();
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // resultado
    return texte.toString();
  }
}

2.7.3. A classe pai [AbstractFragment]

A classe [AbstractFragment] reúne o que é comum a todos os fragmentos da aplicação. Tal como na classe [AbstractActivity], o seu código é complexo. Também aqui vamos analisá-lo por etapas.

2.7.3.1. A estrutura


package client.android.architecture.core;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

import java.util.ArrayList;
import java.util.List;

public abstract class AbstractFragment extends Fragment {

  // dados privados ------------------------------------------------------------
  // as subscrições aos observáveis
  private List<Subscription> abonnements = new ArrayList<>();
  // menu do fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates = new MenuItemState[0];
  // ciclo de vida do fragmento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // estado do fragmento
  private CoreState previousState;
  // mapeador jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo de vida do fragmento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
  // tarefas assíncronas
  private boolean runningTasksHaveBeenCanceled;

  // dados  acessíveis às classes filhas ---------------------------------------
  // modo de depuração
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // nome da classe
  protected String className;
  // tarefas assíncronas
  protected int numberOfRunningTasks;
  // atividade
  protected IMainActivity mainActivity;
  protected Activity activity;
  // sessão
  protected Session session;


  // atualização do fragmento ----------------------------------------------------------------------------------
 ...

  // gestão do menu ------------------------------------------
  ...

  // gestão da espera -------------------------------------------------------------
...

  // gestão de operações assíncronas --------------------------------------------------------------------
...

  // gestão de exceções -------------------------------------------------------------------
....

  // gestão do ciclo de vida do fragmento --------------------------------------------------------
...

  // classes filhas -----------------------------------------------------
  public abstract CoreState saveFragment();

  protected abstract int getNumView();

  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();

  protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);

}
  • linhas 28-45: os dados privados da classe;
  • linhas 47-58: os dados protegidos acessíveis pelas classes filhas;
  • linhas 61-62: código que atualiza o fragmento a ser exibido;
  • linhas 64-65: código utilitário para gerir o eventual menu;
  • linhas 67-68: código utilitário para gerir a espera durante uma operação assíncrona;
  • linhas 70-71: código para facilitar a comunicação do fragmento com a camada [DAO];
  • linhas 73-74: código utilitário para gerir qualquer exceção de forma padrão;
  • linhas 76-77: código que gere o ciclo de vida do fragmento;
  • linhas 80-94: a classe pai impõe 8 métodos às suas classes filhas;

2.7.3.2. O construtor

O construtor da classe é o seguinte:


  // nome da classe
  protected String className;
  // ciclo de vida do fragmento
  private boolean fragmentHasToBeInitialized = false;
...
  // construtor ----------------------
  public AbstractFragment() {
    // inicialização
    className = getClass().getSimpleName();
    fragmentHasToBeInitialized = true;
    // registo
    if (isDebugEnabled) {
      Log.d(className, "constructeur");
    }
}
  • linha 9: regista-se o nome da classe filha que aqui é instanciada. Este nome é utilizado em todos os registos da classe pai;
  • linha 10: regista-se que o fragmento está a ser construído. Esta informação será utilizada quando for solicitado ao fragmento filho que se atualize;

2.7.3.3. Gestão do menu

Na nossa arquitetura, todos os fragmentos devem ter um menu, mesmo que este esteja vazio. Os registos demonstraram, de facto, que quando o método [onCreateOptionsMenu] — executado quando o fragmento possui um menu — é executado, o fragmento já foi associado à sua atividade, à sua vista e ao seu menu, e está prestes a tornar-se visível. Trata-se, portanto, de um momento em que a atualização da interface visual e do menu pode ser efetuada. É neste método [onCreateOptionsMenu] que solicitamos ao fragmento filho que se atualize.

A gestão do menu reúne métodos utilitários que permitem ao fragmento filho exibir ou não elementos do menu:


  // menu do fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
...
  // gestão do menu ------------------------------------------
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // percorre todos os itens do menu
    for (int i = 0; i < menu.size(); i++) {
      // item n.º i
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // se o item n.º i for um submenu, então recomeça-se
      if (menuItem.hasSubMenu()) {
        // recursividade
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
  }

  private void getMenuOptionsStates(Menu menu) {
    // resultado
    if (isDebugEnabled) {
      Log.d(className, "getMenuOptionsStates(Menu)");
    }
    // recuperam-se os identificadores das opções do menu
    List<Integer> menuOptionsIds = new ArrayList<>();
    getMenuOptions(menu, menuOptionsIds);
    // transferimos as opções do menu para uma matriz
    menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // identificador da opção
      int id = menuOptionsIds.get(i);
      // estado da opção
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // resultado
    if (isDebugEnabled) {
      Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
    }
  }

  // estados das opções do menu
  private MenuItemState[] getMenuOptionsStates() {
    MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // estado
      MenuItemState state = this.menuOptionsStates[i];
      // ID do menu
      int id = state.getMenuItemId();
      // inicialização do estado
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // resultado
    return menuOptionsStates;
  }

  // exibição das opções do menu -----------------------------------
  protected void setAllMenuOptionsStates(boolean isVisible) {
    // atualizam-se todas as opções do menu
    for (MenuItemState menuItemState : menuOptionsStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
    }
  }

  protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
    // atualizam-se algumas opções do menu
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}
  • linhas 6-18: este método permite obter os identificadores numéricos de todas as opções do menu;
  • linha 6: o método [getMenuOptions] recebe dois parâmetros:
    • [Menu menu]: o menu do fragmento;
    • [List<Integer> menuOptionsIds]: a lista de identificadores Android das opções do menu. Inicialmente, esta lista está vazia. É preenchida posteriormente através de uma percorrida recursiva (linha 15) da árvore do menu;
  • linhas 20-40: a partir do menu, constrói a matriz de estados (identificador, visibilidade) das opções do menu. Esta matriz é armazenada na linha 3. A classe [MenuItemState] foi descrita no parágrafo 2.7.1;
  • linhas 43-55: uma variante do método anterior. Faz o mesmo, mas em vez de recalcular os identificadores de todas as opções do menu — o que já foi feito —, utiliza os identificadores da tabela de estados da linha 3;
  • linhas 58-63: o método [setAllMenuOptionsStates] permite ocultar ou mostrar todas as opções do menu do fragmento;
  • linhas 65-69: o método [setMenuOptionsStates] permite, de forma seletiva, exibir ou ocultar algumas das opções do menu;
  • os métodos [getMenuOptions, getMenuOptionsStates] são declarados privados, uma vez que são utilizados exclusivamente no [AbstractFragment]. Os métodos [setAllMenuOptionsStates] (linha 58) e [setMenuOptionsStates] (linha 65) são declarados como protegidos para que estejam disponíveis para as classes filhas;

2.7.3.4. Gestão da espera pelo fim de uma tarefa assíncrona


   // as subscrições dos observáveis
  private List<Subscription> abonnements = new ArrayList<>();
// tarefas assíncronas
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...

  // gestão da espera pelo fim de uma operação assíncrona -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // regista-se o número de tarefas que serão executadas
    this.numberOfRunningTasks = numberOfRunningTasks;
    // coloca-se a imagem de espera
    mainActivity.beginWaiting();
    // esvazia-se a lista de subscrições
    abonnements.clear();
    // ainda não houve cancelamento
    runningTasksHaveBeenCanceled = false;
  }

  protected void cancelWaitingTasks() {
    // oculta-se a imagem de espera
    mainActivity.cancelWaiting();
  }

  • linhas 9-18: para iniciar uma ou mais operações assíncronas, o fragmento filho chamará o método pai [beginRunningTasks]. O parâmetro deste método é o número de tarefas assíncronas que o fragmento filho irá iniciar;
  • linha 11: o parâmetro do método é armazenado;
  • linha 13: a imagem de espera é tornada visível;
  • linha 15: limpa-se a lista de subscrições às operações assíncronas. Estas ainda não foram criadas pelo fragmento filho;
  • linha 17: mantém-se um valor booleano para indicar que as tarefas assíncronas solicitadas pelo fragmento filho foram canceladas. Inicialmente, esse valor booleano tem o valor false;
  • linhas 20-25: o fragmento filho chama o método pai [cancelWaitingTasks] para indicar que pretende cancelar as tarefas que iniciou;
  • linha 22: a imagem de espera é ocultada;

2.7.3.5. Gestão de exceções


  // gestão de exceções -------------------------------------------------------------------

  // exibição de alerta sobre exceção
  protected void showAlert(Throwable th) {
    // exibindo as mensagens da pilha de exceções do Throwable th
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
  }

  // exibição da lista de mensagens
  protected void showAlert(List<String> messages) {
    // exibe a lista de mensagens
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
  • linhas 4-7: o método [showAlert(Throwable)] permite que um fragmento filho exiba numa janela as mensagens da pilha de exceções do Throwable passado como parâmetro;
  • linhas 10-13: o método [showAlert(List<String>] permite que um fragmento filho exiba numa janela a lista de mensagens passada como parâmetro;
  • a classe [Utils] utilizada nas linhas 6 e 12 foi descrita no parágrafo 2.7.2;

2.7.3.6. Gestão de operações assíncronas


...
  // subscrições de observáveis
  private List<Subscription> abonnements = new ArrayList<>();
  // tarefas assíncronas
  private boolean runningTasksHaveBeenCanceled;
  protected int numberOfRunningTasks;
...
  // execução de uma tarefa assíncrona com RxAndroid
  protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
    // processo: o observável a ser executado/observado
    // consumeResult: o método que processa a resposta obtida
    // 
    // só se criam novas subscrições se não tiver havido qualquer cancelamento
    if (!runningTasksHaveBeenCanceled) {
      // execução no thread de E/S e observação no thread da interface do utilizador
      process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
      // executa-se o observável
      try {
        abonnements.add(process.subscribe(
          // consumo do resultado
          consumeResult,
          // consumo de exceção
          new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
              consumeThrowable(th);
            }
          },
          // fim da tarefa
          new Action0() {

            @Override
            public void call() {
              endOfTask();
            }
          }));
      } catch (Throwable th) {
        consumeThrowable(th);
      }
    }
  }

  private void endOfTask() {
...
  }

  // uma operação assíncrona lançou uma exceção
  // ou ocorreu uma exceção durante a execução de uma operação assíncrona
  private void consumeThrowable(Throwable th) {
...
  }

  • linhas 9-41: executam uma tarefa assíncrona;
  • linha 9: o método [executeInBackground] espera dois parâmetros:
    • [Observable<T> process]: o processo assíncrono a executar;
    • [Action1<T> consumeResult]: o método do fragmento filho a ser chamado para lhe transmitir os elementos emitidos pelo processo. Nos nossos exemplos anteriores, os processos emitiram sempre apenas um elemento. O tipo T de [Action1<T>] é o tipo T do resultado devolvido pelo processo observado;
  • linha 14: a tarefa assíncrona só é iniciada se ainda não tiver ocorrido uma anulação pelo utilizador ou pelo programa (devido a uma exceção);
  • linha 16: o processo está configurado para ser executado num thread de E/S e é observado no thread da interface do utilizador;
  • linha 16: a instrução [process.subscribe] inicia a execução do processo na thread de E/S. Dentro desta thread, as operações são executadas de forma síncrona, porque utilizamos uma biblioteca HTTP que é síncrona;
  • linha 19: o método [process.subscribe] tem três parâmetros:
    • linha 21: [consumeResult]: o método do fragmento filho que irá consumir os elementos emitidos pelo processo;
    • linhas 22-28: o método executado quando ocorre uma exceção durante o processamento da tarefa assíncrona. O processamento é delegado ao método [consumeThrowable] da linha 49;
    • linhas 29-36: o método executado quando a tarefa emite a notificação de fim de emissão. O processamento é delegado ao método [endOfTask] da linha 43;
  • linha 19: a tarefa assíncrona que acaba de ser iniciada é registada no campo [abonnements], que regista todas as tarefas assíncronas iniciadas. Isto permitirá cancelá-las, se necessário;
  • linhas 37-39: método executado quando ocorre uma exceção durante o processamento da tarefa assíncrona. O processamento é delegado ao método [consumeThrowable] da linha 49;

O método [endOfTask] é o seguinte:


  // tarefas assíncronas
  protected int numberOfRunningTasks;
...
  private void endOfTask() {
    // uma tarefa a menos para esperar
    numberOfRunningTasks--;
    // terminado?
    if (numberOfRunningTasks == 0) {
      // fim da espera
      cancelWaitingTasks();
      // a conclusão das tarefas é comunicada à classe filha
      notifyEndOfTasks(false);
    }
  }
...
  // classes filhas -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • linha 6: uma tarefa assíncrona acaba de ser concluída. O contador de tarefas ativas é decrementado;
  • linha 8: se já não houver tarefas ativas, então o fragmento filho obteve todas as suas respostas;
  • linha 10: a espera é cancelada;
  • linha 12: notifica-se o fragmento filho de que todas as tarefas que lançou estão concluídas, chamando o seu método [notifyEndOfTasks]. O parâmetro deste método indica como as tarefas terminaram: normalmente, por cancelamento do utilizador ou do código, ou devido a uma exceção. Na linha 12, é sinalizado um fim normal. Note-se que o fragmento filho não precisa de se preocupar em manter um registo das tarefas ainda ativas. A sua classe pai faz isso por ele;

O método [consumeThrowable] é o seguinte:


  // tarefas assíncronas
  protected int numberOfRunningTasks;
  private boolean runningTasksHaveBeenCanceled;
...
    // uma operação assíncrona gerou uma exceção
  // ou ocorreu uma exceção durante a execução de uma operação assíncrona
  private void consumeThrowable(Throwable th) {
    // th: a exceção a tratar
    // 
    // registo
    if (isDebugEnabled) {
      Log.d(className, "Exception reçue");
    }
    // cancelam-se as tarefas já iniciadas
    cancelRunningTasks();
    // são apresentadas as mensagens de erro
    showAlert(th);
  }

  // cancelamento das tarefas
  protected void cancelRunningTasks() {
    // registo
    if (isDebugEnabled) {
      Log.d(className, "Annulation des tâches lancées");
    }
    // cancela-se todas as tarefas assíncronas registadas
    for (Subscription abonnement : abonnements) {
      abonnement.unsubscribe();
    }
    // regista-se o cancelamento
    runningTasksHaveBeenCanceled = true;
    numberOfRunningTasks = 0;
    // fim da espera
    cancelWaitingTasks();
    // notifica-se o cancelamento das tarefas ao fragmento filho
    notifyEndOfTasks(true);
}

...
  // classes filhas -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • linha 3: o método [consumeThrowable] recebe a exceção que ocorreu;
  • linha 15: todas as tarefas ainda ativas são canceladas;
  • linha 17: é exibido o texto da exceção;
  • linhas 21-37: cancelamento de todas as tarefas;
  • linhas 27-29: todas as subscrições são canceladas;
  • linha 31: regista-se que houve uma anulação;
  • linha 32: o contador de tarefas é repostado a zero;
  • linha 34: a espera é cancelada;
  • linha 36: notifica-se o fragmento filho do fim das tarefas devido ao cancelamento;

2.7.3.7. Gestão do ciclo de vida do fragmento


  // ciclo de vida --------------------------------------------------------
  @Override
  public void onDestroyView() {
    // pai
    super.onDestroyView();
    // registo
    if (isDebugEnabled) {
      Log.d(className, "onDestroyView");
    }
  }

  @Override
  public void onDestroy() {
    // pai
    super.onDestroy();
    // registo
    if (isDebugEnabled) {
      Log.d(className, "onDestroy");
    }
  }

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }

  private void saveState() {
...
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
...
  }


  @Override
  public void onSaveInstanceState(final Bundle outState) {
...
}
  • linhas 2-20: os métodos [onDestroyView, onDestroy] existem apenas para fins de registo. Estes permitem ao programador compreender melhor o ciclo de vida dos fragmentos;

O armazenamento do fragmento durante uma rotação do dispositivo é realizado pelos seguintes métodos [setUserVisibleHint, onSaveInstanceState, saveState]:


  // ciclo de vida do fragmento
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
...

@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // pai
    super.setUserVisibleHint(isVisibleToUser);
    // guardar?
    if (this.isVisibleToUser && !isVisibleToUser) {
      // o fragmento vai ser ocultado — estamos a guardá-lo
      if (!saveFragmentDone) {
        saveState();
      }
    }
    // memória
    this.isVisibleToUser = isVisibleToUser;
  }

  private void saveState() {
...
  }

  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // registo
    if (isDebugEnabled) {
      Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
    }
    // pai
    super.onSaveInstanceState(outState);
    // guardar o fragmento apenas se estiver visível
    if (isVisibleToUser) {
      // talvez o fragmento já tenha sido guardado
      if (!saveFragmentDone) {
        saveState();
      }
      // restauração a efetuar em todos os casos
      session.setAction(ISession.Action.RESTORE);
    }
}
  • linhas 6-19: o fragmento é guardado se passar do estado «exibido» para o estado «oculto» (linha 11). É o método [setUserVisibleHint] que nos fornece esta informação;
  • linha 14: o salvamento é efetuado pelo método privado das linhas 21-23;
  • linhas 25-41: durante uma rotação do dispositivo, o método [onSaveInstanceState] será chamado. O fragmento é guardado sob duas condições:
    • se estiver visível (linha 34);
    • ainda não tenha sido guardado (linha 36). É possível que os métodos [setUserVisibleHint, onSaveInstanceState] não possam ser executados ambos quando o fragmento está visível e que, por isso, a gestão do valor booleano [saveFragmentDone] seja desnecessária. Em caso de dúvida, optei por utilizar este;
  • linha 40: após o salvamento, seguirá-se a restauração. Note-se que, da próxima vez que o fragmento tiver de ser atualizado, tal deverá ser feito através de uma operação [RESTORE];

Deve-se tomar nota dos dois momentos em que é solicitada uma gravação do fragmento:

  1. quando este passa do estado visível para o estado oculto;
  2. quando ocorre uma rotação do dispositivo;

O método privado [saveState] é o seguinte:


...
  private void saveState() {
    // tarefas a cancelar?
    if (numberOfRunningTasks != 0) {
      // cancelam-se as tarefas
      cancelRunningTasks();
    }
    // guardamos o estado do fragmento
    CoreState currentState = saveFragment();
    // o fragmento foi visitado
    currentState.setHasBeenVisited(true);
    // guardar o estado do menu
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // início de sessão
    session.setCoreState(getNumView(), currentState);
    // gravação efetuada
    saveFragmentDone = true;
    // registo
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }


...
  // classes filhas -----------------------------------------------------
public abstract CoreState saveFragment();

protected abstract int getNumView();
  • linhas 4-7: a rotação do dispositivo pode ocorrer enquanto estão a decorrer operações assíncronas. Nesta altura, decide-se cancelar todas elas. Esta não é uma boa decisão para o utilizador, que terá de voltar a efetuar um novo pedido, potencialmente demorado, quando apenas moveu o telemóvel ou o tablet, ou recebeu uma chamada telefónica. É possível manter as ligações de rede ao longo de um ciclo de cópia de segurança/restauração. No entanto, as soluções não são óbvias e decidi não as abordar neste curso para principiantes. O caminho a seguir é estabelecer essas ligações de rede através de um fragmento sem interface visual associada e que não seja destruído durante o ciclo de cópia de segurança/restauração. Para tal, basta utilizar a instrução [Fragment.setRetainInstance(true)];
  • linha 9: solicita-se ao fragmento filho que guarde o seu estado num tipo derivado de [CoreState] (linha 31);
  • linha 11: regista-se que o fragmento foi visitado. Esta informação é útil. Quando um fragmento é visitado pela primeira vez, a sua atualização pode ser diferente das seguintes, uma vez que, nessa altura, não possui um estado anterior na sessão;
  • linha 13: guarda-se o estado do menu, o que nos permitirá restaurá-lo automaticamente;
  • linha 15: este estado atual é guardado na sessão. Nesta, os estados são agrupados por vista/fragmento, tendo cada um deles um estado. O número da vista é fornecido pelo fragmento filho (linha 33);
  • linha 17: verifica-se que o fragmento foi guardado. Isto porque dois métodos podem chamar o método [saveState] e não faz sentido efetuar dois guardamentos;

A regeneração da vista associada ao fragmento é assegurada pelo seguinte método:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // classe pai
    super.onActivityCreated(savedInstanceState);
    // registo
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // a vista deve ser restaurada
    viewHasToBeInitialized = true;
}

No ciclo de vida, o método [onActivityCreated] é executado imediatamente após o método [onCreateView]. A chamada deste último método indica que a vista associada ao fragmento deve ser reconstruída. Basta registar isso na linha 10.

2.7.3.8. Atualização do fragmento

A atualização do fragmento é a última operação realizada no fragmento antes de este ficar visível e ficar à espera das ações do utilizador. É assegurada pelo código seguinte:


  // menu do fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo de vida do fragmento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // estados do fragmento
  private CoreState previousState;
  // mapeador jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo de vida do fragmento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...

  // Atualização do fragmento ----------------------------------------------------------------------------------
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // registo
    if (isDebugEnabled) {
      Log.d(className, "onCreateOptionsMenu");
    }
    // memória
    this.menu = menu;
    // recuperam-se as # opções do menu, caso ainda não tenha sido feito
    if (fragmentHasToBeInitialized) {
      // recuperam-se as # opções do menu
      getMenuOptionsStates(menu);
      // atividade
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = (Session) this.mainActivity.getSession();
    }
    // recupera-se o estado anterior do fragmento (na primeira vez, apenas o valor booleano hasBeenVisited tem significado)
    previousState = session.getCoreState(getNumView());
    // atualização do fragmento filho em várias etapas
    // etapa 1 — trata-se da primeira visita?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
  ...
    } else {
      // não é a primeira visita
      // etapa 2: o fragmento deve ser inicializado?
      ...
      // etapa 3: a vista deve ser inicializada?
      ...
    }
    // Etapa 4: um envio, uma navegação, uma restauração?
    ...

    // Etapa 5: atualizações finais ----------------------
...
  }
...
  // classes filhas -----------------------------------------------------
  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();
  • linha 19: é utilizado o método [onCreateOptionsMenu] para atualizar o fragmento. Por este motivo, o fragmento deve ter um menu, vazio se necessário. Quando este método é executado, o fragmento já foi associado à sua vista e à sua atividade e, além disso, está visível;
  • linha 25: guarda-se o menu que foi passado como parâmetro (linha 22) ao método;
  • linhas 27-34: se o fragmento tiver de ser inicializado:
    • linha 29: os estados das opções do menu são colocados na tabela [menuOptionsStates] da linha 3;
    • linha 31: a atividade é armazenada como uma instância do tipo Android [Activity];
    • linha 32: a atividade é armazenada como uma instância da interface [IMainActivity];
    • linha 33: a sessão é guardada. A alteração do tipo é necessária, pois o método [mainActivity.getSession()] devolve um tipo [ISession];
  • linha 36: recupera-se da sessão o estado anterior do fragmento. Se for a primeira visita ao fragmento, apenas o valor booleano [previousState.hasBeenVisited] tem significado;
  • linhas 39-44: código executado quando se trata da primeira visita ao fragmento. Neste caso, o seu estado anterior não é significativo;
  • linhas 44-50: código executado quando não se trata da primeira visita ao fragmento;
  • linhas 46-47: código executado se o construtor do fragmento tiver sido chamado (fragmentHasToBeInitialized==true);
  • linhas 48-49: código executado se a vista associada ao fragmento tiver sido reconstruída (viewHasToBeInitialized==true);
  • linhas 51-52: código executado de acordo com a ação (SUBMIT, NAVIGATION, RESTORE) em curso;
  • linhas 54-55: código sempre executado;

As cinco etapas da atualização são as seguintes:

etapa 1


  // menu do fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo de vida do fragmento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // estados do fragmento
  private CoreState previousState;
  // mapeador jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo de vida do fragmento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // recupera-se o estado anterior do fragmento (na primeira vez, apenas o valor booleano hasBeenVisited tem significado)
    previousState = session.getCoreState(getNumView());
    // atualização do fragmento filho em várias etapas
    // etapa 1 — trata-se da primeira visita?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
      // inicialização do fragmento e da vista
      initFragment(null);
      initView(null);
      // reinicialização de previousState para a continuação
      previousState = null;
    } else {
      // não é a primeira visita
...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • linha 19: o estado anterior do fragmento é recuperado da sessão;
  • linhas 22-31: código executado se o fragmento nunca tiver sido visitado;
  • linha 27: solicita-se à classe filha que inicialize o fragmento. O parâmetro do método [initFragment] da linha 35 é o estado anterior do fragmento. Aqui, passa-se null para indicar ao fragmento filha que se trata da primeira visita;
  • linha 28: solicita-se à classe filha que inicialize a vista associada ao fragmento. O parâmetro do método [initView] da linha 37 é o estado anterior do fragmento. Aqui, passa-se null para indicar ao fragmento filha que se trata da primeira visita;
  • linha 30: define-se o estado anterior como null para as etapas seguintes;

etapas 2 e 3


// menu do fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo de vida do fragmento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // estados do fragmento
  private CoreState previousState;
  // mapeador jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo de vida do fragmento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // recupera-se o estado anterior do fragmento (na primeira vez, apenas o valor booleano hasBeenVisited tem significado)
    previousState = session.getCoreState(getNumView());
    // atualização do fragmento filho em várias etapas
    // etapa 1 — trata-se da primeira visita?
    if (!previousState.getHasBeenVisited()) {
...
    } else {
      // não é a primeira visita
      // etapa 2: o fragmento deve ser inicializado?
      if (fragmentHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation fragment");
        }
        // fragmento filho
        initFragment(previousState);
      }
      // Etapa 3: a vista deve ser inicializada?
      if (viewHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation vue");
        }
        // fragmento filho
        initView(previousState);
      }
    }

...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • linhas 24-42: executadas quando não se trata da primeira visita ao fragmento;
  • linhas 27-33: se o fragmento acabou de ser reconstruído, reinicializa-se chamando o método [initFragment] da classe filha (linhas 32, 46). Passa-se-lhe o estado anterior do fragmento;
  • linhas 35-51: se a vista associada ao fragmento tiver de ser inicializada ou reinicializada, solicita-se ao fragmento filho que o faça (linhas 40, 48). Mais uma vez, passa-se-lhe o último estado conhecido do fragmento;

etapa 4


// menu do fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo de vida do fragmento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // Estados do fragmento
  private CoreState previousState;
  // mapeador jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo de vida do fragmento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // recupera-se o estado anterior do fragmento (na primeira vez, apenas o valor booleano hasBeenVisited tem significado)
    previousState = session.getCoreState(getNumView());
    // atualização do fragmento filho em várias etapas
 ...

    // etapa 4: um envio, uma navegação, uma restauração?
    // registo
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
        Log.d(className, String.format("état précédent=%s", jsonMapper.writeValueAsString(previousState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // ação em curso
    ISession.Action action = session.getAction();
    switch (action) {
      case SUBMIT:
        if (isDebugEnabled) {
          Log.d(className, "updateOnSubmit");
        }
        // fragmento filho
        updateOnSubmit(previousState);
        break;
      case NAVIGATION:
        if (isDebugEnabled) {
          Log.d(className, "updateForNavigation");
        }
        if (previousState != null) {
          // restauração do menu
          setMenuOptionsStates(previousState.getMenuOptionsState());
          // fragmento filho
          updateOnRestore(previousState);
        } else {
          // trata-se de uma primeira visita – nada a fazer
        }
        break;
      case RESTORE:
        // restauração
        if (isDebugEnabled) {
          Log.d(className, "updateOnRestore");
        }
        // restauração do menu (previousState não pode ser nulo)
        setMenuOptionsStates(previousState.getMenuOptionsState());
        // fragmento filho
        updateOnRestore(previousState);
        break;
    }
....
  protected abstract void updateOnSubmit(CoreState previousState);

protected abstract void updateOnRestore(CoreState previousState);
  • linhas 34-66: processa-se a ação em curso, que pode ser uma das três seguintes:
    • RESTORE: está a ser efetuada uma restauração do fragmento após uma rotação do dispositivo;
    • NAVIGATION: regressa-se ao fragmento com o objetivo de o encontrar no estado em que foi deixado da última vez que foi utilizado;
    • SUBMIT: todos os outros casos;
  • linha 34: recupera-se a ação em curso;
  • linhas 36-42: para uma ação do tipo SUBMIT, chama-se o método [updateOnSubmit] do fragmento filho (linhas 41, 68), passando-lhe o último estado conhecido do fragmento;
  • linhas 43-55: para uma ação do tipo NAVIGATION;
  • linhas 47-54: pretendemos restabelecer o fragmento no seu último estado conhecido. A operação NAVIGATION pode ocorrer em conjunto com uma primeira visita. Seria o caso, por exemplo, numa aplicação com separadores: se eu passar do separador 1 para o separador 4:
    • tenho de inicializar o fragmento da aba 4, se for a primeira visita;
    • restaurar o fragmento da aba 4 ao seu estado anterior, caso não se trate da primeira visita;
  • linhas 52-54: não se faz nada se for a primeira visita. Caberá ao método filho [initView(CoreState previousState)] efetuar essa inicialização. A primeira visita é caracterizada pela condição [previousState==null];
  • linha 49: se não for a primeira visita ao fragmento, restaura-se o seu menu;
  • linha 51: solicita-se à classe filha que se atualize, chamando o método da linha 70. Passa-se-lhe o estado anterior do fragmento para que possa realizar o seu trabalho;
  • linhas 56-66: no caso de uma operação de restauração do fragmento, faz-se o mesmo que no caso de uma navegação fora da primeira visita;

etapa 5


// menu do fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo de vida do fragmento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // estados do fragmento
  private CoreState previousState;
  // mapeador jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo de vida do fragmento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // etapa 5: atualizações finais ----------------------
    // mudámos de perspetiva
    session.setPreviousView(getNumView());
    // não há mais ações em curso
    session.setAction(ISession.Action.NONE);
    // quando sairmos deste fragmento, este deverá ser guardado
    saveFragmentDone = false;
    // enquanto o fragmento não for reconstruído, não precisa de ser inicializado
    fragmentHasToBeInitialized = false;
    // enquanto a vista não for reconstruída, não precisa de ser inicializada
    viewHasToBeInitialized = false;
    // volta-se ao funcionamento normal da seleção de separadores
    session.setNavigationOnTabSelectionNeeded(true);

    // é comunicado ao fragmento que a vista está pronta
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • linhas 18-30: quando se chega aqui, o fragmento já foi inicializado e está pronto para ser apresentado. Repor-se, então, todos os indicadores utilizados na gestão do ciclo de vida do fragmento ao seu estado inicial;
  • linha 20: mudámos de vista: registamos isso na sessão;
  • linha 22: já não há nenhuma ação em curso;
  • linha 24: quando se sair do fragmento agora exibido, será necessário guardá-lo ao sair;
  • linha 26: o fragmento já não precisa de ser reconstruído. Este indicador será reposto para vrai quando o construtor do fragmento for novamente executado;
  • linha 28: a vista associada ao fragmento já não precisa de ser inicializada. Este indicador será reposto para vrai quando o método [onActivityCreated] for executado novamente;
  • linha 30: o fragmento é apresentado, possivelmente numa aplicação com separadores. Neste caso, quando o utilizador clicar num deles, deve ocorrer uma mudança de fragmento;
  • linha 36: indica-se à classe filha que o fragmento está pronto. Esta pode incluir no método [notifyEndOfUpdates] as atualizações que devam ser feitas em todos os casos, iniciar uma operação assíncrona para obter novos dados, ...

2.7.4. Um exemplo de fragmento

  

Incluímos no projeto [client-android-skel] um exemplo de fragmento para mostrar ao leitor a estrutura típica de um fragmento de uma aplicação baseada neste projeto.

A classe [DummyFragment] é a seguinte:


package client.android.fragments.behavior;

import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;

public class DummyFragment extends AbstractFragment {

  // campos herdados da classe pai -------------------------------------------------------

  // modo de depuração
  //-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // nome da classe
  //-- String protegida className;
  // tarefas assíncronas
  //-- protegido int numberOfRunningTasks;
  // atividade
  //-- protegido IMainActivity mainActivity;
  //-- atividade protegida;
  // sessão
  //-- protegido Session session;

  // métodos herdados da classe pai -------------------------------------------------------

  // exibição das opções do menu
  //-- protegido void setAllMenuOptionsStates(boolean isVisible) {
  //-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
  // gestão da espera pelo fim de uma série de tarefas assíncronas
  //-- protected void beginRunningTasks(int numberOfRunningTasks) {
  //-- protected void cancelWaitingTasks() {
  // execução de uma tarefa assíncrona com RxAndroid
  //-- protegido <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
  // cancelamento de tarefas
  //-- protegido void cancelRunningTasks() {
  // exibição de alerta em caso de exceção
  //-- protected void showAlert(Throwable th) {
  // exibição da lista de mensagens
  //-- protected void showAlert(List<String> mensagens) {

  // métodos impostos pela classe pai -------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    // é necessário guardar o fragmento
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // senão houver nada para guardar, execute [return new CoreState();] e elimine a classe [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // é necessário devolver o n.º do fragmento na tabela de fragmentos geridos pela atividade (ver MainActivity)
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // o fragmento torna-se visível e foi construído nesta etapa ou numa etapa anterior
    // isto ocorre no arranque da aplicação e a cada rotação do dispositivo Android
    // é necessariamente seguida pela execução de [initView]
    // é necessário inicializar os campos do fragmento que foi reconstruído
    // previousState é o último registo do fragmento — tem o valor null se for a primeira visita ao fragmento
  }

  @Override
  protected void initView(CoreState previousState) {
    // o fragmento torna-se visível e a vista associada foi reconstruída nesta etapa ou numa etapa anterior
    // isto ocorre sempre que [initFragment] é executado e sempre que o fragmento sai da adjacência do fragmento exibido
    // é necessário inicializar os componentes da vista que foi reconstruída
    // previousState é o último registo do fragmento — tem o valor null se for a primeira visita ao fragmento

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // é executado após [initFragment, initView] se estes métodos forem executados
    // a vista será apresentada após uma operação do tipo SUBMIT
    // em geral, é necessário inicializar o fragmento e a vista associada a partir da sessão
    // previousState é o último registo do fragmento — tem o valor null se for a primeira visita ao fragmento
    // não há nada a fazer se não for possível aceder ao fragmento através de uma operação SUBMIT
    // se for possível aceder ao fragmento através de operações SUBMIT a partir de fragmentos diferentes, é possível conhecer a vista anterior através de [session.getPreviousView]
    // se for possível chegar ao fragmento através de várias operações SUBMIT a partir do mesmo fragmento, então é necessário ativar um indicador para diferenciar os diferentes tipos de SUBMIT a partir desse fragmento
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // é executada após [initFragment, initView], caso estes métodos sejam executados
    // a vista será apresentada após uma operação do tipo RESTORE ou NAVIGATION
    // previousState é o último registo do fragmento — nunca é nulo
    // é necessário restaurar a vista ao seu estado anterior

  }

  @Override
  protected void notifyEndOfUpdates() {
    // ocorre após os métodos [updateOnSubmit, updateOnRestore]
    // nesta altura, a vista já foi construída e inicializada
    // muitas vezes não há nada a fazer aqui, mas também é possível agrupar aqui as ações que teriam de ser realizadas independentemente da forma como se acede a esta vista
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // chamada quando as tarefas assíncronas iniciadas pelo fragmento estão concluídas ou foram canceladas
    // estes dois casos podem ser diferenciados graças ao parâmetro runningTasksHaveBeenCanceled
    // em geral, é necessário restabelecer a vista num estado diferente daquele em que se encontrava enquanto aguardava as respostas das tarefas assíncronas

  }
}

A classe [DummyFragment] pode não ter um estado. Aqui, incluímos um para recordar o que se espera que contenha:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class DummyFragmentState extends CoreState {
  // estado do fragmento [DummyFragment]
  // incluir apenas campos serializáveis em jSON
  // colocar a anotação @JsonIgnore nos restantes, mas não se percebe bem para que poderiam servir
  // não se esqueça dos getters/setters — são utilizados para a serialização/deserialização
}

Para ilustrar a utilização do projeto [client-android-skel], vamos começar por utilizar exemplos simples antes de passarmos a um estudo de caso mais completo.

2.8. Exercícios ilustrativos

Vamos começar por refatorar exemplos já escritos.

2.8.1. Exemplo-17B

Retomamos o exemplo 17 analisado no parágrafo 1.18. Trata-se de uma aplicação com um único fragmento, sem tarefas assíncronas e sem separadores. Analisamo-la para ver como se comporta durante uma rotação do dispositivo. Introduzimos os seguintes dados:

Image

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

Image

Se compararmos as visualizações, tudo foi mantido, exceto a lista [2], que agora está vazia.

Além disso, se clicarmos no botão [Valider], surge uma caixa de diálogo que mostra os dados introduzidos no formulário. Se, nesse momento, rodarmos o dispositivo, a caixa de diálogo desaparece.

Por isso, durante uma rotação, teremos de regenerar:

  • a lista suspensa e o seu elemento selecionado;
  • a caixa de diálogo, caso estivesse a ser apresentada no momento da rotação;

2.8.1.1. O projeto [Exemple-17B]

Duplicamos o projeto [client-android-skel] em exemplos/Exemplo-17B. Em seguida, carregamos o novo projeto [1]:

  • no [2-3], na pasta [behavior], colamos o fragmento [Vue1Fragment] do projeto [Exemple-17];
  • em [4-5], na pasta [layout] de [Exemple-17B], colamos a vista [vue1.xml] de [Exemple-17]. Esta é a vista associada ao fragmento;
  • em [6], a pasta [values] de [Exemple-17B] é substituída pela pasta [values] de [Exemple-17];

A margem superior da vista [vue1.xml] será alterada para 80 dp:


    <TextView
      android:id="@+id/textViewFormulaireTitre"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      android:layout_marginLeft="10dp"
      android:layout_marginTop="80dp"
      android:text="@string/titre_vue1"
android:textSize="30sp"/>

Nesta fase, pode-se tentar uma primeira compilação para verificar os erros. Os primeiros erros assinalados provêm do ficheiro imports, devido a pacotes que mudaram de localização. Corrigem-se esses erros (Ctrl-Shift-O). Outros erros devem-se ao facto de a vista [Vue1Fragment] não implementar todos os métodos exigidos pela sua classe pai [AbstractParent]:

Image

Geramos os métodos em falta (Alt-Enter).

Outro erro de compilação sinalizado é o seguinte:

Image

Corrige-se isto no ficheiro [build.gradle] do módulo (linha 20 abaixo):

 

Nesta fase, pode-se recompilar para verificar os erros restantes. O único erro assinalado diz respeito ao método [Vue1Fragment.updateFragment]:

 

É necessário eliminar a anotação [@Override] da linha 135. Já não existem erros. Vamos partir daqui para modificar o projeto.

2.8.1.2. O estado do fragmento [Vue1Fragment]

O fragmento [Vue1Fragment] precisa de guardar informações durante a rotação do dispositivo, para que possa ser restaurado na íntegra. Para tal, criamos uma classe [Vue1FragmentState]:

  

Por enquanto, esta classe está vazia:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {
  
}

2.8.1.3. Personalização do projeto

  

Na pasta [custom] encontram-se os elementos de arquitetura personalizáveis pelo programador.

As constantes da interface [IMainActivity] serão as seguintes:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // acesso à sessão
  ISession getSession();

  // mudança de vista
  void navigateToView(int position, ISession.Action action);

  // gestão da espera
  void beginWaiting();

  void cancelWaiting();

  // constantes da aplicação -------------------------------------

  // modo de depuração
  boolean IS_DEBUG_ENABLED = true;

  // tempo máximo de espera pela resposta do servidor
  int TIMEOUT = 1000;

  // tempo de espera antes da execução do pedido do cliente
  int DELAY = 0;

  // autenticação básica
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adjacência dos fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra de separadores
  boolean ARE_TABS_NEEDED = false;

  // imagem de espera
  boolean IS_WAITING_ICON_NEEDED = false;

  // número de fragmentos da aplicação
  int FRAGMENTS_COUNT = 1;

}
  • linhas 24-31: a aplicação não utiliza aqui a sua camada [DAO]. Estas constantes não serão utilizadas;
  • linha 34: uma adjacência de fragmentos igual a 1, que é o valor por predefinição. Como a aplicação tem apenas um fragmento (linha 43), este valor não tem importância;
  • linhas 39-40: como não há operações com a camada [DAO], é desnecessário ter uma imagem de espera;
  • linha 37: não se trata de uma aplicação com separadores;
  • linha 43: existe apenas um fragmento;

A classe [Session] é a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore

}

Está vazia. Com efeito, como existe apenas um fragmento, não há necessidade de prever uma comunicação entre fragmentos com uma sessão.

Por fim, a classe [CoreState] é a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // fragmento visitado ou não
  protected boolean hasBeenVisited = false;
  // estado do eventual menu do fragmento
  protected MenuItemState[] menuOptionsState;

  // getters e setters
...
}
  • linhas 11-13: temos de incluir todas as classes derivadas de [CoreState] que memorizam o estado dos diferentes fragmentos. Aqui, existe apenas uma (linha 12);

2.8.1.4. A atividade [MainActivity]

A atividade [MainActivity] apresenta-se atualmente da seguinte forma:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // camada [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessão
  private Session session;

  // métodos da classe pai -----------------------
  @Override
  protected void onCreateActivity() {
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sessão
    this.session = (Session) super.session;
    // tarefa pendente: continuamos as inicializações iniciadas pela classe pai
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // tarefa pendente: definir os fragmentos aqui
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // a fazer: definir aqui os títulos dos fragmentos
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // tarefa pendente: navegação por separadores — definir a vista a apresentar quando o separador n.º [position] for selecionado
  }

  @Override
  protected int getFirstView() {
    // tarefa: definir o n.º da primeira vista (fragmento) a apresentar
    return 0;
  }
}

Os comentários [//todo] indicam o que o programador deve fazer. A classe [MainActivity] evolui da seguinte forma:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // camada [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessão
  private Session session;

  // métodos da classe pai -----------------------
  @Override
  protected void onCreateActivity() {
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sessão
    this.session = (Session) super.session;
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new Vue1Fragment_()};
  }

  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {

  }

  @Override
  protected int getFirstView() {
    return 0;
  }
}

Apenas o método das linhas 41-44 deve ser alterado. Este deve devolver o array dos fragmentos da aplicação. Na linha 43, não se deve esquecer de colocar o sublinhado após o nome do fragmento.

2.8.1.5. O estado do fragmento [FragmentState]

Na sequência dos testes de rotação realizados no projeto [Exemple-17], decide-se memorizar os seguintes elementos do fragmento:

  • a lista de valores da lista suspensa;
  • a posição do elemento selecionado nessa lista;
  • a mensagem exibida pela caixa de diálogo, caso esta esteja presente no momento da rotação;

A classe [Vue1FragmentState] será a seguinte:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

import java.util.List;

public class Vue1FragmentState extends CoreState {

  // os valores da lista suspensa
  private List<String> list;
  // o elemento selecionado na lista suspensa
  private int listSelectedPosition;
  // a mensagem apresentada na caixa de diálogo
  private String message;

  // getters e setters
...
}

2.8.1.6. O fragmento [AbstractFragment]

Atualmente, o ciclo de vida do fragmento é gerido por dois métodos (linhas 6 e 32):


// lista suspensa
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;

  @AfterViews
  void afterViews() {
    // marca-se o primeiro botão
    radioButton1.setChecked(true);
    // o calendário
    datePicker1.setCalendarViewShown(false);
    // o seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

      public void onStopTrackingTouch(SeekBar seekBar) {
      }

      public void onStartTrackingTouch(SeekBar seekBar) {
      }

      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // a lista suspensa
    list = new ArrayList<>();
    list.add("list 1");
    list.add("list 2");
    list.add("list 3");
  }
...
  protected void updateFragment() {
    // inicialização do adaptador da lista suspensa
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    dropDownList.setAdapter(dataAdapter);
  }

O código destes dois métodos será migrado para os métodos definidos pela classe [AbstractFragment] da seguinte forma:


// gestão do ciclo de vida do fragmento ---------------------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    Vue1FragmentState state = new Vue1FragmentState();
    state.setList(list);
    state.setListSelectedPosition(dropDownList.getSelectedItemPosition());
    state.setMessage(message);
    return state;
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // 1.ª visita?
    if (previousState == null) {
      // criam-se os valores da lista suspensa
      list = new ArrayList<>();
      list.add("list 1");
      list.add("list 2");
      list.add("list 3");
    } else {
      // recuperação dos valores da lista suspensa
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      list = state.getList();
      // e a mensagem da caixa de diálogo
      message = state.getMessage();
    }
    // inicialização do adaptador da lista suspensa
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
  }

  @Override
  protected void initView(CoreState previousState) {
    // o calendário
    datePicker1.setCalendarViewShown(false);
    // o seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

      public void onStopTrackingTouch(SeekBar seekBar) {
      }

      public void onStartTrackingTouch(SeekBar seekBar) {
      }

      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // inicialização do adaptador da lista suspensa
    dropDownList.setAdapter(dataAdapter);
    // Primeira visita?
    if (previousState == null) {
      // marca-se o primeiro botão
      radioButton1.setChecked(true);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // valor da barra de deslocamento
    seekBarValue.setText(String.valueOf(seekBar.getProgress()));
    // elemento selecionado na lista suspensa
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    dropDownList.setSelection(state.getListSelectedPosition());
    // caixa de diálogo visível?
    if (message != null) {
      // é apresentada
      showMessage();
    }
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

}
  • linhas 2-9: o método [saveFragment] deve colocar os elementos do fragmento a memorizar numa classe derivada de [CoreState] e devolver a instância desta;
  • linhas 11-14: o método [getNumView] deve devolver o número do fragmento. Neste caso, existe apenas um fragmento, cujo número é 0;
  • linhas 16-34: o método [initFragment] deve inicializar os campos do fragmento. Recebe o estado anterior do fragmento. Se [previousState] for igual a null, então trata-se da primeira visita;
  • linhas 19-25: na primeira visita, criam-se os valores da lista suspensa;
  • linhas 26-30: se não se tratar da primeira visita, os campos [list, message] do fragmento são restaurados a partir do estado anterior;
  • linhas 33-34: inicialização do campo [dataAdapter] do fragmento. Esta é a fonte de dados da lista suspensa;
  • linhas 37-62: o método [initView] serve para inicializar os componentes da interface visual. Recebe como parâmetro o estado anterior [previousState]. Se for [previousState==null], então trata-se da primeira visita;
  • encontramos aqui o que existia anteriormente no método [@AfterViews];
  • linhas 57-61: na primeira visita, verifica-se se o primeiro botão de opção está marcado;
  • linhas 64-67: o método [updateOnSubmit] é executado quando a ação em curso é [SUBMIT]. Aqui, não há navegação entre fragmentos e, por isso, não há ação em curso;
  • linhas 69-81: o método [updateOnRestore] é executado quando a ação em curso é [NAVIGATION] ou [RESTORE]. Aqui, não há navegação entre fragmentos e, por isso, não é possível realizar a ação [NAVIGATION];
  • linha 72: recalcula-se (não se restaura) o valor de TextView seekBarValue. Com efeito, durante as rotações, por vezes perdia-se o seu valor;
  • linhas 74-75: posiciona-se a lista no elemento que estava selecionado antes da rotação. Sem isso, a lista posicionava-se no seu primeiro elemento;
  • linhas 76-80: volta a exibir-se a caixa de diálogo se a mensagem do estado anterior não for null. Voltaremos ao método [showMessage] (linha 79);
  • linhas 83-86: o método [notifyEndOfUpdates] é o último método chamado pela classe pai antes de deixar o fragmento filho em paz. Aqui não há nada a fazer;
  • linhas 88-91: o método [notifyEndOfTasks] sinaliza o fim das tarefas assíncronas iniciadas pelo fragmento. Aqui, não há nenhuma;

A restauração da caixa de diálogo é feita da seguinte forma:


  // a mensagem da caixa de diálogo
  private String message;
...
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // lista de mensagens a exibir
    List<String> messages = new ArrayList<>();
    ...
    // exibição
    doAfficher(messages);
  }

  private void doAfficher(final List<String> messages) {
    // construção do texto a exibir
    StringBuilder texte = new StringBuilder();
    for (String message : messages) {
      texte.append(String.format("%s\n", message));
    }
    // armazenar a mensagem
    message = texte.toString();
    // exibe-se
    showMessage();
  }

  private void showMessage() {
    // exibe-se
    new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        // reinicialização da mensagem
        message = null;
      }
    }).show();
}

Quando o utilizador valida o formulário, o método [doValider] (linha 5) cria uma lista de mensagens que, em seguida, exibe (linha 10) na caixa de diálogo.

  • linhas 14-20: a lista de mensagens é concatenada numa única mensagem que é armazenada na linha 2;
  • linhas 25-33: é esta mensagem que a caixa de diálogo exibe e é esta mesma mensagem que o método [updateOnRestore] faz exibir;
  • linha 27: o segundo parâmetro do método [setNeutralButton] é o método executado quando o utilizador clica no botão [Fermer] da caixa de diálogo;
  • linha 31: ao fechar a caixa de diálogo, a mensagem é devolvida a null para indicar que a caixa de diálogo já não está presente;

2.8.1.7. Tests

Convida-se o leitor a testar este projeto e a verificar se o fragmento é efetivamente mantido após uma ou várias rotações sucessivas.

2.8.2. Exemplo 23: cliente meteorológico

Alguns sites permitem obter informações meteorológicas sob a forma de cadeias de caracteres como jSON. Eis um exemplo:

Image

O URL tem o seguinte formato: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID}, sendo que:

  • city: a cidade cuja previsão meteorológica se pretende obter, neste caso Angers;
  • country: o país da cidade, neste caso a França (fr);
  • APPID: uma chave obtida ao registar-se no site [https://home.openweathermap.org/users/sign_up];

2.8.2.1. O projeto

  

O projeto foi desenvolvido a partir do projeto [client-android-skel]. Apresenta as seguintes características:

  • tem apenas um fragmento cujo estado não é necessário manter;
  • efectua pedidos assíncronos;

2.8.2.2. Personalização do projeto

  

A interface [IMainActivity] permite especificar determinadas características do projeto:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // acesso à sessão
  ISession getSession();

  // alteração da visualização
  void navigateToView(int position, ISession.Action action);

  // gestão da espera
  void beginWaiting();

  void cancelWaiting();

  // constantes da aplicação -------------------------------------

  // modo de depuração
  boolean IS_DEBUG_ENABLED = true;

  // tempo máximo de espera pela resposta do servidor
  int TIMEOUT = 1000;

  // tempo de espera antes da execução do pedido do cliente
  int DELAY = 5000;

  // autenticação básica
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adjacência dos fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra de separadores
  boolean ARE_TABS_NEEDED = false;

  // imagem de espera
  boolean IS_WAITING_ICON_NEEDED = true;

  // número de fragmentos da aplicação
  int FRAGMENTS_COUNT = 1;

}
  • linhas 25, 28, 31, 40: características da camada [DAO]. Linha 31: não é necessária autenticação básica;
  • linha 34: adjacência dos fragmentos. Neste caso, esta constante não tem importância, uma vez que existe apenas um fragmento;
  • linha 37: não se trata de uma aplicação com separadores;
  • linha 43: existe apenas um fragmento;

A classe [CoreState], que armazena o estado dos fragmentos, será a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// tarefa: adicionar aqui as subclasses de [CoreState]
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // fragmento visitado ou não
  protected boolean hasBeenVisited = false;
  // estado do eventual menu do fragmento
  protected MenuItemState[] menuOptionsState;

  // getters e setters
...
}
  • linhas 10-13: não há nada a declarar, uma vez que nesta aplicação existe apenas um fragmento cujo estado não é guardado;

A classe [Session] é a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
}

Está vazia, uma vez que, nesta aplicação, não existe comunicação entre fragmentos.

2.8.2.3. A camada [DAO]

  

Na camada [DAO], três classes devem ser personalizadas:

  • a interface IDao;
  • a sua implementação Dao;
  • a interface WebClient de comunicação com o servidor web / jSON;

A interface [WebClient] será a seguinte:


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

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

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // serviço meteorológico
  @Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
  String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
  • linhas 18-19: o URL do serviço meteorológico. Recorde-se que esta está relacionada com o URL raiz (RestClientRootUrl, linha 12) do cliente. Aqui, este URL raiz será o [http://api.openweathermap.org/];

A interface [IDao] será a seguinte:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL do serviço web
  void setUrlServiceWebJson(String url);

  // utilizador
  void setUser(String user, String mdp);

  // tempo limite do cliente
  void setTimeout(int timeout);

  // autenticação básica
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // modo de depuração
  void setDebugMode(boolean isDebugEnabled);

  // tempo de espera do cliente, em milissegundos, antes da solicitação
  void setDelay(int delay);

  //  serviço meteorológico
  Observable<String> getWeatherForecast(String city, String country, String APPID);
}
  • Recorde-se que os métodos das linhas 6-22 estão presentes por predefinição na interface IDao do projeto [client-android-skel];
  • linha 25: o método [getWeatherForecast] permite obter a cadeia jSON relativa à previsão meteorológica da cidade [city] do país [country]. O terceiro parâmetro é a chave obtida no site [https://home.openweathermap.org/users/sign_up];

A interface [IDao] é implementada pela seguinte classe [Dao]:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // cliente do serviço web
  @RestService
  protected WebClient webClient;
  // segurança
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // o RestTemplate
  private RestTemplate restTemplate;
  // fábrica do RestTemplate
  private SimpleClientHttpRequestFactory factory;
  // tempo limite
  private int timeout;

  @AfterInject
  public void afterInject() {
    // registo
    Log.d(className, "afterInject");
    // constrói-se o restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // define-se o conversor jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // define-se o restTemplate do cliente web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // define-se o URL do serviço web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // regista-se o utilizador no interceptor
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // memória
    this.timeout = timeout;
    // fábrica de configuração
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // interceptor de autenticação?
    if (isBasicAuthentificationNeeded) {
      // adiciona-se o interceptor de autenticação
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }


  // métodos privados -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // serviço meteorológico ---------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
    // registo
    if (isDebugEnabled) {
      Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
    }
    // resultado
    return getResponse(new IRequest<String>() {
      @Override
      public String getResponse() {
        return webClient.getWeatherForecast(city, country, APPID);
      }
    });
  }
}
  • Recorde-se que as linhas 17-90 estão presentes por predefinição na classe [Dao] do projeto [client-android-skel]. Basta adicionar os métodos de implementação da interface [IDao], específicos da aplicação (linha 92);
  • linhas 93-105: implementação do método [getWeatherForecast]. Esta é muito simples e é realizada em 6 linhas, linhas 100-105;
  • linha 100: o método [getResponse] é um método da classe pai [AbstractDao]. Este método espera um parâmetro do tipo [IRequest<T>], em que T é o tipo da resposta esperada do servidor; neste caso, um String, uma vez que se espera uma cadeia de caracteres jSON. O tipo T de [IRequest<T>] deve ser o tipo T do método [Observable<T> getWeatherForecast];
  • a interface [IRequest<T>] tem apenas um método: getResponse. Este tem como função fornecer a resposta de tipo T que o método [Observable<T> getWeatherForecast] deve devolver;
  • linha 103: é a interface [WebClient] que fornece esta resposta. São-lhe passados os três parâmetros recebidos na linha 94. Por este motivo, estes devem ter o atributo final;

2.8.2.4. A atividade [MainActivity]

  

A atividade [MainActivity] é a seguinte:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.MeteoFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // camada [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // métodos da classe pai -----------------------
  @Override
  protected void onCreateActivity() {
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new MeteoFragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
  }

  @Override
  protected int getFirstView() {
    return 0;
  }

  // interface IDao ---------------------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(String city, String country, String APPID) {
    return dao.getWeatherForecast(city, country, APPID);
  }
}
  • Recorde-se que as linhas 15-55 estão presentes por predefinição no projeto [client-android-skel]. Basta personalizá-las;
  • linhas 37 a 40: a tabela de fragmentos. Aqui existe apenas um;
  • linhas 43-46: não são necessários títulos de fragmentos;
  • linhas 48-50: não há separadores aqui;
  • linhas 52-55: a primeira vista a apresentar é a vista n.º 0, a de [MeteoFragment];
  • linhas 58-61: implementação da interface [IDao]. Aqui, não há nada a fazer além de delegar o trabalho à camada [DAO] da linha 21;

2.8.2.5. O fragmento [MeteoFragment]

  

O fragmento [MeteoFragment] consulta o serviço web / jSON de meteorologia. A sua estrutura é a seguinte:


package client.android.fragments;

import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.AbstractFragment;
import client.android.architecture.MenuItemState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action0;
import rx.functions.Action1;

@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class FirstFragment extends AbstractFragment {
...
}
  • linha 14: a vista [res / layout / meteo_fragment.xml] é a seguinte:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="Construisez votre interface visuelle"
    android:id="@+id/textView" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true" android:layout_marginLeft="64dp" android:layout_marginStart="64dp"
    android:layout_marginTop="120dp"/>
</RelativeLayout>

A vista apresenta apenas o texto da linha 10;

  • linha 15: o menu [res / menu / menu_meteo.xml] é o seguinte:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionMeteo"
        android:title="@string/actionMeteo"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
      <item
        android:id="@+id/actionTerminer"
        android:title="@string/actionTerminer"/>
    </menu>
  </item>
</menu>
  • linhas 10-12: esta opção do menu serve para consultar a previsão meteorológica de uma cidade;
  • linhas 14-15: esta opção do menu serve para cancelar este pedido, caso esteja em curso;
  • linhas 16-18: esta opção do menu encerra a aplicação;

O código completo do fragmento é o seguinte:


package client.android.fragments.behavior;

import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action1;

@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class MeteoFragment extends AbstractFragment {

  // dados locais
  private int nbReponsesRecues;

  // gestão de eventos ---------------------------------------------------------------------------------------
  // cidades cuja previsão meteorológica se pretende obter
  final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

  @OptionsItem(R.id.actionMeteo)
  protected void doMeteo() {
    // o seu país
    String country = "fr";
    // obtenha um identificador API ao criar uma conta [https://home.openweathermap.org/users/sign_up]
    String APPID = "xyz";
    // URL do serviço web / jSON
    mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
    // início da espera pelas tarefas assíncronas [paysDeLoire.length]
    beginWaiting(paysDeLoire.length);
    // número de respostas recebidas
    nbReponsesRecues = 0;
    // as chamadas assíncronas são efetuadas em paralelo
    for (String city : paysDeLoire) {
      // previsão meteorológica
      executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
        @Override
        public void call(String response) {
          // análise da resposta
          consumeResponse(response);
          // uma resposta positiva
          nbReponsesRecues++;
        }
      });
    }
  }

  // análise da resposta do servidor
  private void consumeResponse(String response) {
    // registo
    Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
  }

  // início da espera
  protected void beginWaiting(int numberOfRunningTasks) {
    // registo
    if (isDebugEnabled) {
      Log.d(className, "beginWaiting");
    }
    // pai
    beginRunningTasks(numberOfRunningTasks);
    // é apresentada a opção [Annuler]
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{
      new MenuItemState(R.id.menuActions, true),
      new MenuItemState(R.id.actionAnnuler, true)});

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu
    initMenu();
    // exibição de resultados
    String message;
    switch (nbReponsesRecues) {
      case 0:
        message = "Aucune réponse n'a été reçue";
        break;
      case 1:
        message = "Une réponse a été reçue. Consultez vos logs...";
        break;
      default:
        message = String.format("%s réponses ont été reçues. Consultez vos logs...", nbReponsesRecues);
        break;
    }
    Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
  }

  // métodos privados -----------------------------------
  private void initMenu() {
    if (isDebugEnabled) {
      Log.d(className, "initMenu");
    }
    // menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  // gestão do ciclo de vida ---------------------------------------------------------------------------------------
...
}
  • linhas 25-50: gestão do clique na opção de menu [Météo];
  • linha 32: construção do URL do serviço web / jSON do serviço meteorológico. Este é depois passado para a camada [DAO] através da atividade;
  • linha 34: inicia-se a espera. Passa-se o número de tarefas que vão ser lançadas, para que a classe pai nos possa sinalizar o fim das mesmas. Aqui, há cinco tarefas, pois vamos solicitar a previsão meteorológica das cinco cidades da linha 23;
  • linha 16: vamos contar o número de respostas recebidas para poder exibi-lo;
  • linhas 38-50: percorremos as cidades cuja previsão meteorológica pretendemos obter;
  • linha 40: vamos efetuar 5 pedidos HTTP em paralelo;
  • linha 40: solicitamos à classe pai [AbstractParent] que interroga o serviço web /jSON;
  • linhas 40-48: o método [executeInBackground] espera dois parâmetros:
    • linha 40: o processo a observar e a executar é fornecido pelo método [mainActivity.getWeatherForecast];
    • linhas 40-48: a instância [Action1] a ser executada quando se receber a resposta do serviço assíncrono. O tipo T de [Action1<T>] deve ser o tipo T do resultado do método [getWeatherForecast];
  • linha 44: foi recebida uma resposta. Esta é passada para o método [consumeResponse] da linha 53;
  • linha 46: incrementa-se o contador de respostas recebidas;
  • linhas 53-56: utilização de uma resposta jSON do serviço meteorológico;
  • linha 55: limita-se a registar a cadeia jSON;
  • linhas 59-72: código executado antes do lançamento das tarefas assíncronas;
  • linha 65: passa-se o número de tarefas a executar para a classe pai [AbstractParent]. É isto que permite que esta nos avise quando todas estiverem concluídas;
  • linhas 67-70: preparação do menu para um período de espera. Mantém-se apenas a opção [Actions/Annuler], que permitirá ao utilizador cancelar as tarefas iniciadas;
  • linhas 74-92: código executado quando a classe pai nos avisa que todas as tarefas iniciadas estão concluídas;
  • linha 77: repõe-se o menu no seu estado inicial. O método [initMenu] (linhas 95-102) apresenta o menu com todas as suas opções, exceto a opção [Actions/Annuler], que fica oculta;
  • linhas 80-91: exibe-se o número de respostas recebidas;

O clique na opção de menu [Annuler] é gerido pelo código seguinte:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // cancelar tarefas assíncronas
    cancelRunningTasks();
}
  • linha 7: solicita-se à classe pai que cancele as tarefas ainda ativas;

O clique na opção de menu [Terminer] é gerido pelo código seguinte:


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminer() {
    // parar tudo
    System.exit(0);
}

A gestão do ciclo de vida do fragmento é assegurada pelos seguintes métodos:


  // gestão do ciclo de vida ---------------------------------------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {

  }

  @Override
  protected void initView(CoreState previousState) {
    // 1.ª visita?
    if (previousState == null) {
      initMenu();
    }
  }


  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

}
  • linhas 3-6: servem para memorizar o estado do fragmento numa classe derivada de [CoreState]. Se o fragmento não tiver um estado a memorizar, como neste caso, basta devolver uma instância de [CoreState]. Não se deve devolver null, pois isso provocaria posteriormente uma falha do sistema;
  • linhas 8-11: devem devolver o número da vista. Aqui, o fragmento [MeteoFragment] tem o número 0;
  • linhas 13-16: servem para inicializar o fragmento depois de este ter sido construído (previousState==null) ou reconstruído (previousState!=null). Aqui, não há nada a fazer. O único campo passível de inicialização é o seguinte:

  // cidades cuja previsão meteorológica se pretende obter
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

mas este inicializa-se automaticamente;

  • linhas 18-24: servem para inicializar a vista associada ao fragmento assim que este tiver sido construído (previousState==null) ou reconstruído (previousState!=null);
  • linhas 21-23: se for a primeira visita ao fragmento, inicializa-se o seu menu para ocultar a opção [Annuler];
  • linhas 27-30: chamadas se, para chegar ao fragmento, tiver havido navegação com uma ação do tipo [SUBMIT]. Aqui, não há navegação entre fragmentos, uma vez que existe apenas um fragmento;
  • linhas 32-35: chamadas durante um ciclo de gravação/restauração devido a uma rotação do dispositivo ou a outro motivo. Aqui, como não foi gravado nenhum estado, não há nada a fazer;
  • linhas 37-40: chamadas quando todas as atualizações anteriores tiverem sido efetuadas. Aqui, não há nada a fazer;

2.8.2.6. Tests

Vamos agora executar o exemplo:

Image

Image

Os registos são então os seguintes:


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

Agora, fazemos a consulta com um identificador API incorreto:


    String APIID = "";

Image

Os registos são então os seguintes:


07-23 13:34:43.853 11240-11240/client.android D/MeteoFragment_: beginWaiting
...
07-23 13:34:49.121 11240-11464/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.121 11240-11466/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11468/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11467/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception reçue
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Annulation des tâches lancées
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: initMenu
07-23 13:34:49.167 11240-11465/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
  • linhas 3-6, 10: as 5 chamadas HTTP geraram 5 exceções;
  • linha 7: o fragmento [MeteoFragment] recebe a primeira exceção. Irá, então, cancelar todas as tarefas;

Agora, vamos definir um tempo de espera de 5 segundos para [IMainActivity.DELAY] e cancelar a operação. Os registos ficam então assim:


07-21 13:16:20.329 20390-20390/client.android D/MeteoFragment_: beginWaiting
...
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation demandée
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation des tâches lancées
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: initMenu
07-21 13:25:02.948 29965-30197/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
  • linha 3: pedido de cancelamento;
  • linha 4: a espera é cancelada porque ocorreu um cancelamento;
  • linhas 6-10: a anulação das tarefas provoca uma exceção em cada uma das threads das cinco tarefas. O tipo de exceção depende das aplicações. A exceção aqui é [java.lang.InterruptedException] porque as tarefas foram interrompidas enquanto executavam a instrução [Thread.sleep(delay)], que as faz esperar artificialmente [delay] milissegundos;

2.8.3. Exemplo 16B

Reestruturamos aqui o exemplo 16 do parágrafo 1.17. Este apresenta um fragmento que efetua chamadas assíncronas a um servidor de números aleatórios. Vejamos como se comporta durante uma rotação do dispositivo:

Image

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

Image

Vemos que perdemos todas as mensagens de erro. Vamos tentar melhorar isto.

2.8.3.1. O projeto Exemplo-16B

Copiamos o projeto [client-android-skel] para o projeto [exemples/Exemple-16B] e, em seguida, carregamos o novo projeto:

  

Do projeto inicial [Exemple-16], copiamos para o [Exemple-16B] os seguintes elementos:

  • o ficheiro [res/layout/vue1.xml], a pasta [res/values]:
  

Alteraremos a margem superior da vista [vue1.xml] para 80 dp:


  <TextView
    android:id="@+id/txt_Titre2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="80dp"
    android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
  • o fragmento [Vue1Fragment]:
  
  • a classe [dao / service / Response]:
  

Nesta fase, podemos tentar uma primeira compilação:

  • um primeiro tipo de erros é o das classes imports. Algumas classes mudaram de pacote na migração para [Exemple-16B]. Começamos por corrigir este tipo de erros;
  • um segundo tipo de erro é sinalizado na classe [Vue1Fragment] porque esta não implementa os métodos exigidos pela classe pai [AbstractParent]. Procedemos à geração automática desses métodos;

Tentamos uma segunda compilação:

  • todos os erros restantes estão agora concentrados na classe [Vue1Fragment], a classe que irá sofrer mais alterações;

2.8.3.2. Criação de um estado para o fragmento [Vue1Fragment]

Vimos que algumas informações do fragmento teriam de ser guardadas durante uma rotação, a fim de restaurar o fragmento tal como estava antes da rotação. Por isso, criamos um estado [Vue1FragmentState], vazio por enquanto:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {

}

2.8.3.3. Personalização do projeto

  

A interface [IMainActivity] permite especificar determinadas características do projeto:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // acesso à sessão
  ISession getSession();

  // alteração da visualização
  void navigateToView(int position, ISession.Action action);

  // gestão da espera
  void beginWaiting();

  void cancelWaiting();

  // constantes da aplicação -------------------------------------

  // modo de depuração
  boolean IS_DEBUG_ENABLED = true;

  // tempo máximo de espera pela resposta do servidor
  int TIMEOUT = 1000;

  // tempo de espera antes da execução do pedido do cliente
  int DELAY = 5000;

  // autenticação básica
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adjacência dos fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra de separadores
  boolean ARE_TABS_NEEDED = false;

  // imagem de espera
  boolean IS_WAITING_ICON_NEEDED = true;

  // número de fragmentos da aplicação
  int FRAGMENTS_COUNT = 1;

}
  • linhas 25, 28, 31, 40: características da camada [DAO]. Não é necessária autenticação básica;
  • linha 34: adjacência dos fragmentos. Neste caso, esta constante não tem importância, uma vez que existe apenas um fragmento;
  • linha 37: não se trata de uma aplicação com separadores;
  • linha 43: existe apenas um fragmento;

A classe [CoreState], que armazena o estado dos fragmentos, será a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // fragmento visitado ou não
  protected boolean hasBeenVisited = false;
  // estado do eventual menu do fragmento
  protected MenuItemState[] menuOptionsState;

  // getters e setters
...
}
  • linha 12: declaramos a classe do estado do fragmento [Vue1Fragment];

A classe [Session] é a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
}

Está vazia, uma vez que, nesta aplicação, não existe comunicação entre fragmentos.

2.8.3.4. A camada [DAO]

  

Na camada [DAO], três classes devem ser personalizadas:

  • a interface IDao;
  • a sua implementação Dao;
  • a interface WebClient de comunicação com o servidor web / jSON;

A classe [Response] provém do projeto [Exemple-16], que a utiliza:


package client.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- propriedades
    // estado da operação
    private int status;
    // eventuais mensagens de erro
    private List<String> messages;
    // o corpo da resposta
    private T body;

    // construtores
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getters e setters
...
}

A interface [WebClient] será a seguinte:


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

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

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // 1 número aleatório no intervalo [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);

}
  • linhas 18-19: o URL do serviço de números aleatórios. Recorde-se que esta está relacionada com o URL raiz (RestClientRootUrl, linha 12) do cliente. Aqui, este URL raiz será [http://localhost:8080];

A interface [IDao] será a seguinte:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL do serviço web
  void setUrlServiceWebJson(String url);

  // utilizador
  void setUser(String user, String mdp);

  // tempo limite do cliente
  void setTimeout(int timeout);

  // autenticação básica
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // modo de depuração
  void setDebugMode(boolean isDebugEnabled);

  // tempo de espera do cliente, em milissegundos, antes da solicitação
  void setDelay(int delay);

  // serviço de números aleatórios
  Observable<Response<Integer>> getAlea(int a, int b);

}
  • Recorde-se que os métodos das linhas 6-22 estão presentes por predefinição na interface IDao do projeto [client-android-skel];
  • linha 25: o método [getAlea] permite obter um número aleatório no intervalo [a,b]. Este número é obtido numa resposta do tipo [Response<Integer>], em que o número aleatório se encontra no campo [body] desse tipo;

A interface [IDao] é implementada pela seguinte classe [Dao]:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // cliente do serviço web
  @RestService
  protected WebClient webClient;
  // segurança
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // o RestTemplate
  private RestTemplate restTemplate;
  // fábrica do RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // registo
    Log.d(className, "afterInject");
    // fabrica-se o restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // fixa-se o conversor jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // define-se o restTemplate do cliente web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // define-se o URL do serviço web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // regista-se o utilizador no interceptor
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // configuração de fábrica
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // interceptor de autenticação?
    if (isBasicAuthentificationNeeded) {
      // adiciona-se o interceptor de autenticação
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // métodos privados -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // serviço de números aleatórios
  @Override
  public Observable<Response<Integer>> getAlea(final int a, final int b) {
    // execução do cliente web
    return getResponse(new IRequest<Response<Integer>>() {
      @Override
      public Response<Integer> getResponse() {
        return webClient.getAlea(a, b);
      }
    });
  }

}
  • Recorde-se que as linhas 17-85 estão presentes por predefinição na classe [Dao] do projeto [client-android-skel]. Basta adicionar os métodos de implementação da interface [IDao];
  • linhas 88-97: implementação do método [getAlea]. Esta é muito simples e é realizada em 6 linhas, linhas 91-96;
  • linha 91: o método [getResponse] é um método da classe pai [AbstractDao]. Este método espera um parâmetro do tipo [IRequest<T>], em que T é o tipo da resposta esperada, neste caso um tipo Response<Integer>. O tipo T de [IRequest<T>] (linha 91) deve ser o tipo T do método [Observable<T> getAlea] (linha 89);
  • A interface [IRequest<T>] tem apenas um método: getResponse. Este tem como função fornecer a resposta do tipo T que o método [Observable<T> getAlea] deve devolver;
  • linha 94: é a interface [WebClient] que fornece esta resposta. São-lhe passados os dois parâmetros recebidos na linha 89. Por este motivo, estes devem ter o atributo «final»;

2.8.3.5. A atividade [MainActivity]

  

A atividade [MainActivity] é a seguinte:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // camada [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // métodos da classe pai -----------------------
  @Override
  protected void onCreateActivity() {
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // continuamos as inicializações iniciadas pela classe pai
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // definir os fragmentos aqui
    return new AbstractFragment[]{new Vue1Fragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // definir aqui os títulos dos fragmentos
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // navegação por separadores - definir a vista a apresentar
  }

  @Override
  protected int getFirstView() {
    return 0;
  }

  // interface IDao ------------------------------------------
  @Override
  public Observable<Response<Integer>> getAlea(int a, int b) {
    return dao.getAlea(a, b);
  }

}
  • Recorde-se que as linhas 15-61 estão presentes por predefinição no projeto [client-android-skel]. Basta personalizá-las;
  • linhas 40-44: a tabela de fragmentos. Aqui existe apenas um;
  • linhas 47-51: não são necessários títulos de fragmentos;
  • linhas 53-56: não há separadores aqui;
  • linhas 58-61: a primeira vista a apresentar é a vista n.º 0, a de [Vue1Fragment];
  • linhas 64-67: implementação da interface [IDao]. Aqui, não há nada a fazer além de delegar o trabalho à camada [DAO] da linha 23;

2.8.3.6. O estado do fragmento [Vue1Fragment]

  

A classe [Vue1FragmentState] será a seguinte:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

import java.util.ArrayList;
import java.util.List;

public class Vue1FragmentState extends CoreState {

  // estado do fragmento ------------------------
  // lista de respostas
  private List<String> reponses = new ArrayList<>();
  // estado da vista ------------------------
  // mensagem de erro relativa ao número de números aleatórios solicitados
  private boolean txtErrorAleasVisible = false;
  // mensagem de erro sobre o intervalo de geração [a,b]
  private boolean txtErrorIntervalleVisible = false;
  // mensagem de erro relativa ao URL do serviço web
  private boolean txtMsgErreurUrlServiceWebVisible = false;
  // mensagem de erro relativa ao tempo de espera
  private boolean textViewErreurDelayVisible = false;
  // estado visível ou não do botão Executar
  private boolean btnExecuterVisible = true;

  // getters e setters
...
}

Para determinar o que era necessário memorizar no fragmento, realizámos rotações do dispositivo em várias situações e observámos o que desaparecia durante a restauração. Chegámos à conclusão de que era necessário memorizar as informações das linhas 10 a 23.

2.8.3.7. O fragmento [Vue1Fragment]

  

Atualmente, a vista [Vue1Fragment] apresenta vários erros devido ao facto de a classe pai [AbstractFragment], da qual deriva, ter sido alterada. Em vez de descrever uma a uma as alterações a efetuar, vamos comentar diretamente a versão final.

A estrutura do fragmento é a seguinte:


package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.*;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.dao.service.Response;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.Observable;
import rx.functions.Action1;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vide)
public class Vue1Fragment extends AbstractFragment {

...
}
  • na linha 26, recorde-se que qualquer fragmento deve ter um menu, mesmo que esteja vazio. É o que acontece aqui.

2.8.3.7.1. Gestão do clique no botão [Exécuter]

@Click(R.id.btn_Executer)
  protected void doExecuter() {
    // verifica-se os dados introduzidos
    if (!isPageValid()) {
      return;
    }
    // limpa-se as respostas anteriores
    reponses.clear();
    dataAdapterReponses.notifyDataSetChanged();
    // zerar o contador de respostas
    nbReponses = 0;
    infoReponses.setText("Liste des réponses (0)");
    // inicialização da atividade
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // prepara-se a tarefa aleatória
    beginWaiting(1);
    // solicitam-se os números aleatórios
    getAleasInBackground(nbAleas, a, b);
  }

  void getAleasInBackground(int nbAleas, int a, int b) {
    // cria-se o processo a observar
    Observable<Response<Integer>> process = Observable.empty();
    for (int i = 0; i < nbAleas; i++) {
      process = process.mergeWith(mainActivity.getAlea(a, b));
    }
    // solicitação de números aleatórios
    executeInBackground(process, new Action1<Response<Integer>>() {

      @Override
      public void call(Response<Integer> response) {
        // a resposta é processada
        consumeAleaResponse(response);
      }
    });
  }

  protected void consumeAleaResponse(Response<Integer> response) {
    // registo
    if (isDebugEnabled) {
      try {
        Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // uma resposta de +
    nbReponses++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
    // analisamos a resposta
    // erro?
    if (response.getStatus() != 0) {
      // exibição
      showAlert(response.getMessages());
      // cancelamento
      doAnnuler();
      // regresso à interface do utilizador
      return;
    }
    // adiciona-se a informação à lista de respostas
    reponses.add(0, String.valueOf(response.getBody()));
    // atualizar as respostas
    dataAdapterReponses.notifyDataSetChanged();
  }

  // cancelamento ----------
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // cancelam-se as tarefas assíncronas
    cancelRunningTasks();
}

  private void beginWaiting(int nbRunningTasks) {
    // ativa-se a ampulheta
    beginRunningTasks(nbRunningTasks);
    // o botão [Annuler] substitui o botão [Exécuter]
    btnExecuter.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
  }
  • linhas 4-6: verifica-se primeiro se os dados introduzidos são válidos. Podem então surgir mensagens de erro;
  • linhas 8-9: a lista de respostas é esvaziada. Esta alteração é refletida no ListView, que as apresenta;
  • linhas 11-12: o número de respostas recebidas é zerado;
  • linha 14: define-se o URL do serviço de números aleatórios. Esta informação será transmitida à camada [DAO];
  • linha 15: define-se o tempo de espera antes de efetuar o pedido ao serviço de números aleatórios. Esta informação será transmitida à camada [DAO];
  • linha 17: prepara-se o lançamento de 1 tarefa assíncrona (e não N, veremos porquê);
  • linhas 24-27: das N tarefas assíncronas, transformamos cada uma numa sequência de operações [merge];
  • linhas 29-36: solicita-se à classe pai [AbstractParent] que interroga o serviço web / jSON para obter números aleatórios;
  • linhas 29-36: o método [executeInBackground] espera dois parâmetros:
    • linha 29: o processo a observar e a executar é aquele que foi calculado nas linhas anteriores;
    • linhas 29-36: a instância [Action1] a ser executada quando se receber a resposta do serviço assíncrono. O tipo T de [Action1<T>] deve ser o tipo T do resultado do método [getAlea], ou seja, um tipo [Response<Integer>];
  • linha 34: quando chega uma resposta (um número aleatório), esta é processada no método da linha 39;
  • linhas 49-50: regista-se e sinaliza-se que foi recebida uma nova resposta;
  • linhas 53-60: o tipo [Response<T>] possui um campo [status] que é um código de erro. Se este código for diferente de zero, significa que o servidor encontrou um problema;
  • linha 55: é exibida uma mensagem de erro. O método [showAlert] pertence à classe pai;
  • linha 57: é chamado o método das linhas 68-75. Este irá cancelar as tarefas ainda ativas (linha 74);
  • linha 62: a resposta é adicionada à lista de respostas, que constitui a fonte de dados do ListView;
  • linha 64: o ListView é atualizado;
  • linhas 77-83: o método [beginWaiting(int nbRunningTasks)] prepara a vista para a espera (linhas 81-82) e comunica à classe pai que as tarefas do [nbRunningTasks] serão executadas em breve (linha 79);

2.8.3.7.2. O ciclo de vida do fragmento

O ciclo de vida do fragmento é assegurado pelos seguintes métodos:


  // dados locais
  private List<String> reponses;
  private ArrayAdapter<String> dataAdapterReponses;
  private int nbReponses = 0;
...
  // gestão do ciclo de vida ---------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // estado atual da vista
    Vue1FragmentState state = new Vue1FragmentState();
    state.setTextViewErreurDelayVisible(textViewErreurDelay.getVisibility() == View.VISIBLE);
    state.setTxtErrorAleasVisible(txtErrorAleas.getVisibility() == View.VISIBLE);
    state.setTxtMsgErreurUrlServiceWebVisible(txtMsgErreurUrlServiceWeb.getVisibility() == View.VISIBLE);
    state.setTxtErrorIntervalleVisible(txtErrorIntervalle.getVisibility() == View.VISIBLE);
    state.setBtnExecuterVisible(btnExecuter.getVisibility() == View.VISIBLE);
    state.setReponses(reponses);
    return state;
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // 1.ª visita?
    if (previousState != null) {
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      reponses = state.getReponses();
    } else {
      reponses = new ArrayList<>();
    }
    // fonte de dados do listView
    dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    // N.º de respostas
    nbReponses = reponses.size();
  }

  @Override
  protected void initView(CoreState previousState) {
    // ligação entre a lista de visualização e o adaptador
    listReponses.setAdapter(dataAdapterReponses);
    // Primeira visita?
    if (previousState == null) {
      // ocultar mensagens de erro
      txtErrorAleas.setVisibility(View.INVISIBLE);
      txtErrorIntervalle.setVisibility(View.INVISIBLE);
      txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
      textViewErreurDelay.setVisibility(View.INVISIBLE);
      // os botões
      btnAnnuler.setVisibility(View.INVISIBLE);
      btnExecuter.setVisibility(View.VISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // estado anterior da vista
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    // mostrar/ocultar mensagens de erro
    txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
    txtErrorIntervalle.setVisibility(state.isTxtErrorIntervalleVisible() ? View.VISIBLE : View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
    textViewErreurDelay.setVisibility(state.isTextViewErreurDelayVisible() ? View.VISIBLE : View.INVISIBLE);
    // botões
    btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
    btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
    // número de respostas
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // o botão [Exécuter] substitui o botão [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);

}
  • linhas 7-18: garantem o armazenamento do fragmento quando a classe pai o solicita;
  • linha 11: exibe a mensagem de erro relativa ao tempo de espera;
  • linha 12: exibe a mensagem de erro relativa ao número de números aleatórios solicitados;
  • linha 13: exibe a mensagem de erro relativa ao URL do serviço web / jSON;
  • linha 14: visibilidade da mensagem de erro relativa ao intervalo [a,b] de geração de números aleatórios;
  • linha 15: visibilidade do botão [Exécuter];
  • linha 16: a lista de respostas recebidas;
  • linhas 20-23: devem indicar o número da vista. O número do fragmento é aqui 0, uma vez que só existe um;
  • linhas 25-38: inicialização dos campos do fragmento, quer numa primeira visita (previousState==null), quer numa visita posterior;
    • linhas 29-30: se não for a primeira visita, o campo [reponses] é restaurado a partir do estado anterior do fragmento;
    • linhas 31-33: se for a primeira visita, o campo [reponses] é inicializado com uma lista vazia;
    • linhas 34-37: a partir do campo [reponses], é possível construir a fonte de dados do ListView do fragmento (linha 35), bem como o número de respostas (linha 37);
  • linhas 40-55: executadas para inicializar a vista associada ao fragmento, quer numa primeira visita (previousState==null), quer numa visita posterior;
    • linha 43: associa-se o ListView do fragmento à fonte de dados que acabou de ser criada no método [initFragment];
    • linhas 45-54: se for a primeira visita, prepara-se a vista para a sua primeira exibição;
  • linhas 57-60: executadas durante uma navegação entre fragmentos associada a uma ação do tipo [SUBMIT]. Neste caso, existe apenas um fragmento e, por isso, não há navegação entre fragmentos;
  • linhas 63-76: executadas durante uma navegação entre fragmentos associada a uma ação do tipo [NAVIGATION] ou durante um ciclo de gravação/restauração devido a uma rotação do dispositivo ou a outro motivo. Aqui, apenas este último caso pode ocorrer. É importante lembrar que, neste contexto, em todos os casos, [previousState] é sempre diferente de null;
  • linha 65: converte-se o estado anterior para o tipo do estado do fragmento;
  • linhas 66-75: utiliza-se o conteúdo do estado anterior para restaurar a vista;
  • linhas 78-81: chamadas quando todas as atualizações anteriores tiverem sido efetuadas. Aqui, não há nada a fazer;
  • linhas 83-89: executadas quando todas as tarefas assíncronas estiverem concluídas. Aqui, oculta-se o botão [Annuler] para o substituir pelo botão [Exécuter];

2.8.3.8. Os testes

O leitor é convidado a realizar os seguintes testes:

  • criar erros e colocar o dispositivo em funcionamento: as mensagens de erro devem permanecer visíveis;
  • obter números aleatórios e executar o dispositivo: os números aleatórios obtidos devem permanecer visíveis;
  • definir uma espera de vários segundos e executar o dispositivo durante a espera: as tarefas devem ter sido canceladas (isto é visível nos registos);

2.8.4. Exemplo-22B

Retomamos aqui o exemplo 22 para o refatorar de acordo com o modelo do projeto [client-android-skel]. Recorde-se que o projeto [Exemple-22] gere corretamente o ciclo de gravação/restauração dos fragmentos durante uma rotação e que foi este que serviu de base ao projeto [client-android-skel].

Duplicamos o projeto [client-android-skel] para [exemples/Exemple-22B] e carregamos este último projeto:

  

Em seguida, copiamos vários elementos do projeto [Exemple-22] para o projeto [Exemple-22B].

Em primeiro lugar, copiamos elementos da pasta [res]:

  • [layout/fragment_main.xml, layout/vue1.xml, menu/menu_fragment.xml, menu/menu_main.xml, a pasta [values];
  

Alteraremos a margem superior das duas vistas para 120 dp:

[vue1.xml]:


  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="@string/titre_vue1"
    android:id="@+id/textViewTitreVue1"
    android:layout_marginTop="120dp"
    android:textSize="50sp"
    android:layout_gravity="center|left"
    android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>

[fragment_main]:


  <TextView
    android:id="@+id/section_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
android:layout_marginTop="120dp"/>

Em seguida, copiamos os elementos [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:

 

Nesta fase, podemos tentar uma primeira compilação. Surge um primeiro tipo de erros: os imports incorretos, porque as classes mudaram de pacote. Corrigimos esses imports. Um segundo tipo de erros deve-se ao facto de os fragmentos não implementarem todos os métodos da sua classe pai [AbstractFragment]. Corrige-se premindo (Alt+Enter).

Os erros restantes resultam das diferenças existentes entre a classe antiga e a nova [AbstractFragment]. Por enquanto, ignoramo-los.

2.8.4.1. Personalização do projeto

  

Na pasta [custom] encontram-se os elementos de arquitetura personalizáveis pelo programador.

A interface [IMainActivity] permite especificar determinadas características do projeto:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // acesso à sessão
  ISession getSession();

  // mudança de visualização
  void navigateToView(int position, ISession.Action action);

  // gestão da espera
  void beginWaiting();

  void cancelWaiting();

  // modo de depuração
  boolean IS_DEBUG_ENABLED = true;

  // tempo máximo de espera pela resposta do servidor
  int TIMEOUT = 1000;

  // tempo de espera antes da execução do pedido do cliente
  int DELAY = 0;

  // autenticação básica
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adjacência dos fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra de separadores
  boolean ARE_TABS_NEEDED = true;

  // imagem de espera
  boolean IS_WAITING_ICON_NEEDED = false;

  // número de fragmentos
  int FRAGMENTS_COUNT = 5;

}
  • linhas 23, 26, 29, 38: características da camada [DAO]. Aqui não existem;
  • linha 41: existem aqui cinco fragmentos;
  • linha 32: adjacência dos fragmentos. Esta constante pode assumir aqui um valor em [1,4]. Recomenda-se ao leitor que altere este valor para verificar se a aplicação continua a funcionar;
  • linha 35: trata-se de uma aplicação com separadores;

A classe [CoreState], que armazena o estado dos fragmentos, será a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.PlaceHolderFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = PlaceHolderFragmentState.class)}
)
public class CoreState {
  // fragmento visitado ou não
  protected boolean hasBeenVisited = false;
  // estado do eventual menu do fragmento
  protected MenuItemState[] menuOptionsState;

  // getters e setters
...
}
  • linha 12: declaramos a classe do estado do fragmento [PlaceHolderFragment]. O fragmento [Vue1Fragment], por sua vez, não tem estado;

A classe [Session] é a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // dados a partilhar entre os próprios fragmentos e entre fragmentos e a atividade
  // os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
  // não se esqueça dos getters e setters necessários para a serialização/deserialização em jSON

  // número de fragmentos visitados
  private int numVisit;
  // n.º do fragmento do tipo [PlaceholderFragment] apresentado no segundo separador
  private int numFragment = -1;

  // getters e setters
...
}

Esta é a sessão do projeto [Exemple-22].

2.8.4.2. A atividade [MainActivity]

  

A atividade [MainActivity] é a seguinte:


package client.android.activity;

import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.util.Log;
import android.view.MenuItem;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.PlaceholderFragment_;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // camada [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessão
  private Session session;

  // gestão do menu-----------------------
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
...
  }

  private void showFragment(int i) {
...
  }

  // implementação de métodos da classe pai ---------------------------------------------------
  ...
}

Aqui, a classe [MainActivity] é mais extensa do que a dos exemplos anteriores por duas razões:

  • há separadores para gerir;
  • há um menu para gerir;

2.8.4.2.1. Implementação dos métodos da classe pai

// métodos da classe pai -----------------------
  @Override
  protected void onCreateActivity() {
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // continuamos as inicializações iniciadas pela classe pai
    // sessão
    this.session = (Session) super.session;
    ...
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // n.º do fragmento
    final String ARG_SECTION_NUMBER = "section_number";
    // inicialização da tabela de fragmentos
    AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
    int i;
    for (i = 0; i < fragments.length - 1; i++) {
      // cria-se um fragmento
      fragments[i] = new PlaceholderFragment_();
      // é possível passar argumentos ao fragmento
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, i + 1);
      fragments[i].setArguments(args);
    }
    // um fragmento de +
    fragments[i] = new Vue1Fragment_();
    // resultado
    return fragments;
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // sem títulos aqui
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
...
  }

  @Override
  protected int getFirstView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
  • linhas 2-12: o método [onCreateActivity] é chamado pela classe pai [AbstractActivity] quando a atividade é criada pela primeira vez ou recriada durante um ciclo de gravação/restauração. Quando este método é chamado, a classe pai já restaurou a sessão;
  • linha 10: recupera-se uma referência local da sessão. A alteração do tipo deve-se ao facto de a sessão da classe pai ser do tipo [AbstractSession];
  • linhas 19-38: o método [getFragments] deve devolver à classe pai a matriz dos fragmentos geridos pela aplicação. Aqui, existem [FRAGMENTS_COUNT], número definido em [IMainActivity]. Os primeiros fragmentos [FRAGMENTS_COUNT-1] são do tipo [PlaceHolderFragment] e o último é do tipo [Vue1Fragment];
  • linhas 41-45: o método [getFragmentTitle] deve apresentar os títulos dos fragmentos quando essa informação for útil. Não é esse o caso aqui;
  • linhas 47-50: este método é chamado pela classe pai quando o utilizador clica num separador. Voltaremos a este assunto no parágrafo seguinte;
  • linhas 52-55: devolve o número da primeira vista a apresentar quando a aplicação é iniciada. Neste caso, é o fragmento [Vue1Fragment] que deve ser apresentado em primeiro lugar. O método [getFirstView] poderia ser vantajosamente substituído por uma constante em [IMainActivity];

2.8.4.2.2. Gestão dos separadores

Os separadores são geridos pelos seguintes métodos:


@Override
  protected void onCreateActivity() {
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // continuamos as inicializações iniciadas pela classe pai
    // sessão
    this.session = (Session) super.session;
    // 1.º separador
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // 2.º separador?
    int numFragment = session.getNumFragment();
    if (numFragment != -1) {
      TabLayout.Tab tab2 = tabLayout.newTab();
      tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
      tabLayout.addTab(tab2);
    }
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // n.º do fragmento a apresentar
    int numFragment;
    switch (position) {
      case 0:
        // n.º do fragmento [Vue1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // N.º do fragmento [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // visualização do fragmento
    if (numFragment != mViewPager.getCurrentItem()) {
      navigateToView(numFragment, ISession.Action.SUBMIT);
    }
  }
}
  • linhas 1-20: o método [onCreateActivity] é chamado pela classe pai [AbstractActivity] quando a atividade é criada pela primeira vez ou recriada durante um ciclo de gravação/restauração. Quando este método é chamado, a classe pai já restaurou a sessão;
  • linha 9: recupera-se uma referência local da sessão. A alteração do tipo deve-se ao facto de a sessão da classe pai ser do tipo [AbstractSession];
  • linhas 11-13: cria-se o primeiro separador;
  • linhas 15-20: cria-se o segundo separador se estiver registado um número de fragmento na sessão (linha 15). Este número tem inicialmente o valor -1 aquando da primeira construção da atividade;
  • linhas 23-39: este método é chamado pela classe pai quando o utilizador clica numa aba;
  • linhas 28-31: se for clicado no separador 0, deve ser apresentado [Vue1Fragment]. Sabe-se que esta é a primeira vista que foi apresentada ao iniciar a aplicação;
  • linhas 32-35: se for clicada a aba 1, deve ser exibido o fragmento cujo número está registado na sessão;
  • linhas 37-39: navega-se para o fragmento escolhido. A ação associada é [SUBMIT]. Teria podido ser [NAVIGATION]? Neste documento, utiliza-se [NAVIGATION] apenas quando a exibição do novo fragmento requer apenas o conhecimento do seu estado anterior. Aqui, não é esse o caso, uma vez que a exibição do fragmento em causa tem de mudar em relação ao seu estado anterior para apresentar mais uma visita;

2.8.4.2.3. Gestão do menu

A atividade está associada ao seguinte menu [menu_main.xml]:


<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context="exemples.android.MainActivity">
  <item android:id="@+id/action_settings"
        android:title="@string/action_settings"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment1"
        android:title="@string/fragment1"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment2"
        android:title="@string/fragment2"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment3"
        android:title="@string/fragment3"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment4"
        android:title="@string/fragment4"
        android:orderInCategory="100"
        app:showAsAction="never"/>
</menu>

que apresenta o seguinte:

  

A gestão do menu é assegurada pelos seguintes métodos:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onOptionsItemSelected");
    }
    // processamento das opções do menu
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d(className, "action_settings selected");
        }
        break;
      }
      case R.id.fragment1: {
        showFragment(0);
        break;
      }
      case R.id.fragment2: {
        showFragment(1);
        break;
      }
      case R.id.fragment3: {
        showFragment(2);
        break;
      }
      case R.id.fragment4: {
        showFragment(3);
        break;
      }
    }
    // item processado
    return true;
  }

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // sem navegação na seleção programática de um separador
      session.setNavigationOnTabSelectionNeeded(false);
      // recriamos os dois separadores devido a um problema com o tipo de letra dos títulos
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
      // o número do fragmento a apresentar é definido na sessão
      session.setNumFragment(i);
      // seleciona-se o separador n.º 2 com navegação
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • linhas 16-31: gestão do clique numa opção de menu do tipo [Fragmenti];
  • linhas 37-50: exibem o fragmento n.º i (trata-se de fragmentos do tipo PlaceHolderFragment) no separador n.º 1 (2.º separador);
  • linhas 42-44: decide-se eliminar os separadores existentes para recriar dois novos. Esta decisão foi tomada para contornar o seguinte problema: quando nos limitamos a apresentar o fragmento no separador 1 existente (sem, portanto, o eliminar), curiosamente o seu título tem um aspeto (tipo de letra, tamanho) diferente do título do separador 0;
  • linhas 43-44: as duas separadores são criadas, mas não selecionadas (último parâmetro em false);
  • linha 40: as operações das linhas 42-44 podem realizar operações [select] nas guias, o que irá chamar o gestor [onTabSelected]. Se não se fizer nada, haverá então uma navegação para um fragmento. Isto evita-se definindo o valor booleano [navigationOnTabSelectionNeeded] para faux na sessão. Este valor booleano é automaticamente reposto para vrai pela classe [AbstractFragment] quando um fragmento se torna visível;
  • linha 46: regista-se o n.º do fragmento a apresentar na sessão;
  • linhas 48-50: seleciona-se o separador n.º 2 com navegação (linha 48). Isto irá acionar o procedimento [onTabSelected], que irá:
    • exibir o fragmento cujo número foi colocado na sessão;
    • armazenar na sessão o número do separador selecionado;

2.8.4.3. O fragmento [Vue1Fragment]

Apresentamos aqui a versão final do fragmento:


package client.android.fragments.behavior;

import android.widget.EditText;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {

  // os elementos da interface visual
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  // gestor de eventos
  @Click(R.id.buttonValider)
  protected void doValider() {
    // é apresentado o nome introduzido
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  // ciclo de vida do fragmento -----------------------------------------------
  private void initFragment() {
    // nada a fazer
  }

  // guardar estado do fragmento
  @Override
  public CoreState saveFragment() {
    // estado da vista - nada para guardar
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // nada a fazer
  }

  @Override
  protected void initView(CoreState previousState) {
    // 1.ª visita?
    if (previousState == null) {
      // exibe-se o n.º da visita
      showNumVisit();
    }

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // exibe-se o n.º da visita
    showNumVisit();

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

  // métodos privados -------------------------------------
  // exibição do n.º da visita
  private void showNumVisit() {
    // incrementar o número da visita
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // exibe o número da visita
    Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}

A classe está praticamente vazia.

  • linhas 35-39: chamadas pela classe pai quando o fragmento tem de guardar o seu estado. O fragmento [Vue1Fragment] não tem nenhum estado para guardar. Basta devolver uma instância da classe base [CoreState] (lembre-se: não se deve devolver null);
  • linhas 41-44: devem devolver o n.º do fragmento. O fragmento [Vue1Fragment] tem, por definição, o n.º [FRAGMENTS_COUNT-1];
  • linhas 51-59: chamadas pela classe pai quando o fragmento é construído pela primeira vez (previousState==null) ou nas vezes seguintes (previousState!=null);
    • linhas 54-57: se for a primeira visita, incrementa-se o número de visitas e este é apresentado (linhas 85-92);
  • linhas 61-65: chamadas quando o fragmento vai ser apresentado associado a uma ação [SUBMIT]. Incrementa-se o número de visita e exibe-se o mesmo. Aqui, não é possível que o número de visita seja incrementado duas vezes durante o ciclo de vida. Com efeito, a primeira visita ao fragmento [Vue1Fragment] ocorre no arranque da aplicação, quando a ação é, por definição, [NONE] na sessão. Isto garante que o método [updateOnSubmit] não será chamado. Posteriormente, já não será nunca mais a primeira visita e o método [initView] não fará nada;
  • linhas 68-71: chamadas num ciclo de gravação/restauração. Como o fragmento não tem estado, não há aqui nada para restaurar;
  • linhas 73-76: chamadas quando todas as atualizações anteriores tiverem sido efetuadas. Aqui, não há mais nada a fazer;
  • linhas 78-81: chamadas quando todas as tarefas assíncronas iniciadas estiverem concluídas. Aqui, não há tarefas assíncronas;

2.8.4.4. O estado [PlaceHolderFragmentState]

O estado do fragmento [PlaceHolderFragment] será o seguinte:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class PlaceHolderFragmentState extends CoreState {
  // texto
  private String text;

  // construtores
  public PlaceHolderFragmentState() {

  }

  public PlaceHolderFragmentState(String text) {
    super();
    this.text = text;
  }

  // getters e setters
 ...
}
  • quando for necessário guardar o estado do fragmento, será guardado o texto que este exibia (linha 7);

2.8.4.5. O fragmento [PlaceHolderFragment]

O fragmento [PlaceHolderFragment] será o seguinte:


package client.android.fragments.behavior;

import android.util.Log;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.PlaceHolderFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {

  // componentes da interface visual
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  @ViewById(R.id.textView1)
  protected TextView textView1;

  // dados
  private String text;

  // n.º do fragmento
  private static final String ARG_SECTION_NUMBER = "section_number";

  // implementação dos métodos da classe pai ----------------------------
  @Override
  public CoreState saveFragment() {
    // salva-se o estado do fragmento
    PlaceHolderFragmentState placeHolderFragmentState = new PlaceHolderFragmentState();
    placeHolderFragmentState.setText(textViewInfo.getText().toString());
    return placeHolderFragmentState;
  }

  @Override
  protected int getNumView() {
    return getArguments().getInt(ARG_SECTION_NUMBER) - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // texto original
    text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // atualiza-se o texto apresentado
    // incrementar o número de visitas
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // texto alterado
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
    // registo
    if (isDebugEnabled) {
      Log.d(className, String.format("updateForSubmit, numvisit=%s, texte affiché=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
    }
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // restaura-se o texto apresentado
    PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
    textViewInfo.setText(state.getText());

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

}
  • linhas 30-36: quando a classe pai solicita ao fragmento que guarde o seu estado, guarda-se o texto exibido pelo fragmento (linha 34);
  • linhas 38-41: devolvem o número do fragmento. Este depende do número da secção que lhe foi passado como argumento aquando da sua criação;
  • linhas 43-47: chamadas durante a primeira construção do fragmento (previousState==null) ou nas seguintes (previousState !=null);
    • linha 46: aqui, não se utiliza o estado anterior. O texto inicial [text] (linha 24), apresentado na primeira visita, é recalculado de cada vez. Isto é discutível. Poderia ter-se optado por incluir também esta informação no estado do fragmento;
  • linhas 49-51: chamadas durante a primeira construção da vista associada ao fragmento (previousState==null) ou nas seguintes (previousState!=null). Não há nada a fazer;
  • linhas 53-56: chamadas quando o fragmento vai ser apresentado associado a uma ação [SUBMIT]. É sempre este o caso, exceto no ciclo de gravação/restauração, em que a ação é [RESTORE]. Assim, incrementa-se o número da visita e este é apresentado;
  • linhas 68-74: chamadas num ciclo de gravação/restauração. Restaura-se o texto que tinha sido guardado no estado do fragmento;
  • linhas 76-79: chamadas quando todas as atualizações anteriores tiverem sido efetuadas. Aqui, não há mais nada a fazer;
  • linhas 82-83: chamadas quando todas as tarefas assíncronas iniciadas estiverem concluídas. Aqui, não há tarefas assíncronas;

2.8.4.6. Tests

Convida-se o leitor a testar a aplicação, rodando o dispositivo para verificar se o fragmento apresentado mantém o seu estado. Analisaremos também os registos.

2.9. Conclusion

No final deste capítulo, dispomos de um projeto modelo [client-android-skel] de um cliente Android que comunica com um serviço web / jSON com as seguintes características:

  • a comunicação assíncrona com o servidor web / jSON é feita através da biblioteca RxJava;
  • o ciclo de vida de um fragmento (atualização, gravação, restauração) é gerido pela sua classe pai [AbstractFragment], que invoca, em momentos específicos, determinados métodos das suas classes filhas. Assim, o fragmento filho não precisa de se preocupar com as etapas do ciclo de vida, mas apenas de implementar determinados métodos impostos pela sua classe pai;
  • o ciclo de vida da atividade (guardar/restaurar) é gerido por uma classe abstrata, [AbstractActivity], que também impõe à atividade filha a implementação de determinados métodos;
  • a classe [AbstractActivity] é capaz de gerir uma aplicação com ou sem separadores, com ou sem imagem de espera, com ou sem autenticação básica junto do servidor web / jSON. A presença ou ausência destes elementos é definida por configuração;

Vamos agora apresentar um estudo de caso mais complexo do que os exemplos anteriores. A nova aplicação basear-se-á no projeto modelo [client-android-skel].