Skip to content

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

Apresentamos agora um esboço de aplicativo Android que se comunica com um ou mais serviços web / jSON. Trata-se do projeto [client-android-skel], que pode ser encontrado na pasta [architecture] de exemplos:

  

O estudo desse esqueleto de aplicativo será uma oportunidade para revisar alguns pontos que abordamos nos exemplos anteriores. Este aplicativo servirá de estrutura para todos os aplicativos futuros. Ele foi construído após inúmeras iterações. Seu objetivo é fatorar, em classes abstratas, o maior número possível de elementos dos aplicativos que construiremos em breve, a fim de evitar ter que escrever sempre o mesmo tipo de código, que se diferencia apenas por detalhes. Suas características são as seguintes:

  • a comunicação assíncrona com o servidor web / jSON é feita por meio da biblioteca RxJava;
  • o ciclo de vida de um fragmento (atualização, salvamento, restauração) é gerenciado por sua classe pai [AbstractFragment], que chama, em momentos específicos, determinados métodos de suas classes filhas. Assim, a classe filha não precisa se preocupar com as etapas do ciclo de vida, mas apenas em implementar determinados métodos exigidos por sua classe pai;
  • o ciclo de vida da atividade (salvar/restaurar) é gerenciado por uma classe abstrata [AbstractActivity], que também exige que a atividade filha implemente determinados métodos;
  • a classe [AbstractActivity] é capaz de gerenciar um aplicativo com ou sem abas, com ou sem imagem de espera, com ou sem autenticação básica no servidor web / jSON. A presença ou ausência desses elementos é definida por configuração;

Esse esqueleto foi utilizado em todos os exemplos a seguir. Devido à diversidade deles, o que funcionava em um exemplo poderia não funcionar no exemplo seguinte. Como o esqueleto foi utilizado em sete exemplos no total, ocorreram inúmeras iterações. Se fosse utilizado para um oitavo exemplo, é possível que, mais uma vez, a especificidade desse novo exemplo gerasse novos erros. No entanto, o uso desse esqueleto simplificará consideravelmente a redação dos exemplos a seguir. De fato, o gerenciamento do ciclo de vida de um fragmento (update, save, restore), aliado ao conceito de adjacência entre fragmentos, é particularmente complexo. Aqui, ele está totalmente oculto na classe [AbstractFragment].

2.1. Arquitetura do cliente Android

O cliente Android proposto baseia-se na seguinte arquitetura:

  • a camada [DAO] implementa uma interface [IDao]. É ela que se comunica com o servidor web / jSON;
  • há apenas uma atividade que também implementa a interface [IDao]. As visualizações se comunicam com ela para acessar o servidor;
  • as visualizações são implementadas por fragmentos;

O projeto Android reflete essa arquitetura:

  

Apresentaremos, um por um, os diferentes elementos deste projeto.

2.2. A configuração do Gradle

 

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

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

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

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

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

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

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

2.3. O manifesto do aplicativo

 

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

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

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

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

</manifest>
  • linha 3: alteraremos o pacote do aplicativo;
  • linhas 10, 15: definiremos o valor do item [app_name] no arquivo [res / values / strings.xml]. Por enquanto, ele é o seguinte:

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

  <!-- nome do aplicativo -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

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

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

2.5. Elementos da atividade

 

Image

2.5.1. A visualização associada à atividade

A visualização [activity_main.xml] associada à atividade é a seguinte:


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

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

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

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

A atividade também possui um menu [res / menu / menu_main.xml] para sua visualização:


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

Por enquanto, ele está vazio. O desenvolvedor o preencherá, se necessário.

2.5.2. O contêiner de fragmentos [MyPager]

  

package client.android.architecture;

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

public class MyPager extends ViewPager {

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

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

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

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

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

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

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

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

Essa classe estende a classe padrão do Android [ViewPager] apenas para gerenciar o deslize (linha 11) e a rolagem (linha 13) entre as visualizações.

  • linhas 26-43: os métodos que desativam o deslize caso ele tenha sido desativado;
  • linhas 46-49: redefinição do método [setCurrentItem], que serve para alterar a visualização exibida. Se a rolagem tiver sido desativada, a mudança de visualização ocorrerá sem rolagem. Vale ressaltar que o desenvolvedor pode contornar esse modo de funcionamento utilizando o método [setCurrentItem(int position, boolean smoothScrolling)], que permite especificar a rolagem desejada;

2.5.3. A classe [CoreState]

  

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


package client.android.architecture.custom;

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

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

  // getters e setters
...
}
  • linha 16: cada fragmento possui, em seu estado, um valor booleano [hasBeenVisited] que indica se já foi visitado ou não. Isso é necessário porque, às vezes, na primeira exibição de um fragmento, há ações específicas a serem realizadas;
  • linha 18: o projeto [client-android-skel] salva e restaura automaticamente os menus dos fragmentos, caso eles tenham um. Na tabela MenuItemState[] menuOptionsState, armazena-se o estado de visibilidade de todas as opções do menu;
  • linhas 10-13: assim como foi feito em [Exemple-22], o estado da atividade e de seus fragmentos será salvo na sessão, que, por sua vez, será salva na forma de uma cadeia jSON. Veremos que a sessão armazena um array de elementos do tipo [CoreState]. Se nada for feito, será salva a cadeia jSON de um tipo [CoreState]. No entanto, queremos salvar os estados dos fragmentos, ou seja, os estados derivados de [CoreState]. Para que seja gerada a cadeia jSON do tipo derivado — e não a do tipo pai —, é necessário declarar os tipos derivados conforme indicado nas linhas 10 a 13. A classe [CoreState] é uma das classes da arquitetura que o desenvolvedor deve modificar para cada nova aplicação (linhas 10 a 13);

2.5.4. A interface [IMainActivity]

  

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

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 visualização
  void navigateToView(int position, ISession.Action action);

  // gerenciamento da espera
  void beginWaiting();

  void cancelWaiting();

  // constantes do aplicativo (a serem modificadas) -------------------------------------

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

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

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

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

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

  // barra de abas
  boolean ARE_TABS_NEEDED = false;

  // imagem de espera
  boolean IS_WAITING_ICON_NEEDED = false;

  // número de fragmentos do aplicativo
  int FRAGMENTS_COUNT = 0;

  // tarefa: adicione aqui suas constantes e outros métodos
}
  • linha 6: a interface [IMainActivity] estende a interface [IDao] da camada [DAO];
  • linha 9: é a atividade que fornece acesso à sessão na forma de uma instância da interface [ISession];
  • linha 12: é por meio dessa atividade que se alterna de visualização. O segundo parâmetro é a ação que provoca essa mudança de visualização, um dos valores SUBMIT, NAVIGATION, RESTORE;
  • linhas 15-17: é a atividade que gerencia a imagem de espera;
  • linha 22: para a depuração do aplicativo;
  • linha 25: para não esperar muito tempo caso o servidor não responda mais;
  • linha 28: durante a depuração, definiremos um valor de alguns segundos para ter tempo de cancelar a operação com o servidor e verificar o que está acontecendo;
  • linha 31: para true caso o serviço jSON solicite autenticação básica;
  • linha 34: adjacência de fragmentos;
  • linha 37: para vrai se o aplicativo tiver abas;
  • linha 39: para vrai se o aplicativo se comunicar com um servidor web / jSON e se desejar exibir uma imagem de espera durante as trocas de dados;
  • linha 43: o número de fragmentos gerenciados pelo aplicativo;

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

2.5.5. A interface [IDao]

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

  

package client.android.dao.service;

import rx.Observable;

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

  // usuário
  void setUser(String user, String mdp);

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

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

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

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

  // tarefa: declare sua interface aqui
}
  • linha 24: o desenvolvedor preencherá a interface aqui;

2.5.6. A sessão

  

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


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;

public interface ISession {

  // número da última visualização exibida
  int getPreviousView();

  void setPreviousView(int numView);

  // último estado de uma visualização
  CoreState getCoreState(int numView);

  void setCoreState(int numView, CoreState coreState);

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

  Action getAction();

  void setAction(Action action);

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

  void setCoreStates(CoreState[] coreStates);

  // nº da última aba selecionada
  int getPreviousTab();

  void setPreviousTab(int position);

  // navegação ao selecionar a aba
  boolean isNavigationOnTabSelectionNeeded();

  void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}

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

  • linhas 7-10: o número da última visualização (fragmento) exibida;
  • linhas 12-15: o estado de uma visualização específica;
  • linhas 17-24: introduzimos o conceito de ação em andamento. São quatro (linha 17):
    • RESTORE: um salvamento/restauração está em andamento. Não há mudança de visualização;
    • NAVIGATION: uma navegação está em andamento. Chamaremos aqui de navegação uma mudança de visualização em que a nova visualização pode ser restaurada a partir de seu último estado armazenado na sessão;
    • SUBMIT: atribuiremos o tipo [SUBMIT] a uma ação em andamento quando houver mudança de visualização e a nova visualização depender do estado da atividade em geral, e não apenas de seu próprio estado. Às vezes, é difícil distinguir entre NAVIGATION e SUBMIT. Nesse caso, considera-se o caso mais geral, o SUBMIT;
    • NONE: valor da ação quando esta ainda não recebeu seu primeiro valor;
  • linhas 26-30: os estados da atividade e dos fragmentos serão armazenados em um array do tipo CoreState[]. Para que ele seja gerenciado corretamente durante as serializações/desserializações jSON, é necessário que ele possua um getter e um setter;
  • linhas 32-35: número da última aba selecionada. É usado durante o ciclo de salvamento/restauração para selecionar novamente a aba que estava selecionada antes da rotação do dispositivo;
  • linhas 37-40: gerenciamento de um valor booleano que indica se a seleção de uma aba deve ser acompanhada por uma mudança de fragmento;

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


package client.android.architecture.core;

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

public class AbstractSession implements ISession {
  // nº da visualização anterior
  private int preViousView;

  // status das visualizações
  private CoreState[] coreStates = new CoreState[0];

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

  // guia selecionado anteriormente
  private int previousTab;

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

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


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

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

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

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

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

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

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

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

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

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

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

  @Override
  public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
    this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
  }
}
  • linha 9: o número da visualização que estava exibida antes da atual. Essa informação é útil quando é possível acessar uma visualização a partir de vários locais. Esse é tipicamente o caso na navegação por abas. A visualização exibida pode, então, saber qual era a visualização anterior;
  • linha 12: a tabela de estados de todos os fragmentos exibidos pela atividade;
  • linha 18: o número da aba selecionada anteriormente. Desempenha um papel semelhante ao do número da visualização anterior da linha 9. Essa informação é útil quando há rotação do dispositivo e é necessário retornar à aba que estava selecionada antes da rotação;
  • linha 22: um valor booleano indicando se a seleção de uma aba deve ser acompanhada por uma mudança no fragmento exibido. É importante saber que o projeto [client-android-skel] faz um gerenciamento separado das abas e dos fragmentos para poder ser utilizado em casos em que o número de abas é menor que o número de fragmentos. Existem dois tipos de seleção:
    • uma seleção feita pelo usuário ao clicar em uma aba. Nesse caso, geralmente o fragmento exibido deve mudar;
    • uma seleção por meio do método [Tablayout.Tab.select()]. Nesse caso, a alteração do fragmento exibido nem sempre é desejável. Aqui estão dois exemplos:
      • ao girar o dispositivo, a atividade é recriada, assim como as abas. No entanto, quando a primeira aba é criada, ela passa automaticamente por uma operação de software [select]. Nesse momento, não é desejável alterar o fragmento exibido, pois estamos em uma fase de recriação da atividade em que o fragmento exibido no final não será necessariamente aquele associado à primeira aba;
      • uma vez que o gerenciamento das abas é separado do gerenciamento dos fragmentos, pode-se querer atualizar as abas (exclusão, adição) sem interferir nos fragmentos a elas associados. No entanto, algumas dessas operações podem, mais uma vez, acionar uma operação de software [select] implícita em uma das abas. Essa seleção não deve, portanto, necessariamente resultar em uma navegação para o fragmento associado;
  • linha 21: o campo [navigationOnTabSelectionNeeded] não deve ser salvo durante as operações de salvamento da atividade e de seus fragmentos. A anotação [@JsonIgnore] faz com que o campo seja ignorado durante as serializações/desserializações jSON;
  • linhas 25-31: o construtor inicializa a matriz de estados dos fragmentos [FRAGMENTS_COUNT] do aplicativo. Os elementos dessa matriz são inicializados com o campo [hasBeeenVisited=false]. Essa informação é usada para determinar se se trata ou não da primeira visita ao fragmento;

A classe [Session] é a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // dados a serem compartilhados entre os próprios fragmentos e entre fragmentos e atividades
  // os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
  // não se esqueça dos getters e setters necessários para a serialização/desserialização em jSON
}
  • linha 5: a classe [Session] estende a classe [AbstractSession] que acabamos de ver. O desenvolvedor colocará nela os elementos a serem compartilhados entre os próprios fragmentos e entre os fragmentos e a atividade. Observe-se que a classe [Session] não é mais anotada pela anotação AA [@EBean]. Ela se tornou uma classe normal;

2.5.7. A classe abstrata [AbstractActivity]

  

2.5.7.1. Squelette

A classe [AbstractActivity] tem mais de 300 linhas. Vamos analisá-la em etapas. Sua estrutura básica é a seguinte:


package client.android.architecture;

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

import java.io.IOException;

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

  // o contêiner de fragmentos
  protected MyPager mViewPager;
  // a barra de ferramentas
  private Toolbar toolbar;
  // a imagem de espera
  private ProgressBar loadingPanel;
  // barra de abas
  protected TabLayout tabLayout;

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

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

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

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

  // gerenciamento da imagem de espera ---------------------------------
  ...

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

  // gerenciador de fragmentos --------------------------------
  ...

  // classes derivadas
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

  protected abstract int getFirstView();

}

A classe [AbstractActivity]:

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

2.5.7.2. Implementar a interface [IMainActivity]

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


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

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

2.5.7.3. Salvamento do estado da atividade e de seus fragmentos

O estado da atividade e de seus fragmentos está inteiramente na sessão. Portanto, trata-se de salvar essa sessão. Repetimos aqui o que foi feito no projeto [Exemple-22] (ver parágrafo 1.23):


  // gerenciamento de salvamento/restauração da atividade ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // pai
    super.onSaveInstanceState(outState);
    // salvamento da sessão na forma de uma string jSON
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // log
    if (IS_DEBUG_ENABLED) {
      try {
        Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}

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

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


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

2.5.7.5. Inicialização da camada [DAO]


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

2.5.7.6. Inicialização da visualização associada à atividade

A visualização associada à atividade foi apresentada no parágrafo 2.5.1:


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

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

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

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

Essa visualização é inicializada com o código a seguir:


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

2.5.7.7. Gerenciamento de abas

A interface [IMainActivity] pode solicitar uma barra de abas. Ela é adicionada e gerenciada da seguinte maneira:


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

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

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

        }

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

        }
      });
    }

...
  // classes filhas
  protected abstract void navigateOnTabSelected(int position);
...
  • linhas 12-48: adição e gerenciamento de uma barra de abas;
  • linha 6: a adição da barra de abas ocorre se a constante [ARE_TABS_NEEDED] estiver definida como vrai na interface [IMainActivity];
  • linha 12: durante a criação da barra de abas, podem ocorrer operações [Tablayout.Tab.select] implícitas (não são provocadas pelo usuário). Define-se o booleano [session.navigationOnTabSelectionNeeded] como faux para evitar qualquer navegação durante essas seleções falsas. Caberá ao desenvolvedor selecionar o fragmento a ser exibido com o método [navigateToView]. A variável booleana [session.navigationOnTabSelectionNeeded] será redefinida para vrai quando esse fragmento for exibido (consulte a classe AbstractFragment);
  • linha 14: criação de uma barra de abas referenciada pelo campo [tabLayout]. Utilizamos uma barra de abas personalizada [CustomTabLayout], à qual voltaremos mais adiante;
  • linha 15: definimos as cores dos títulos das abas. Elas estão no seguinte arquivo [res / color / tab_txt.xml]:

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

Esse arquivo é, obviamente, editável. Os códigos hexadecimais das cores podem ser encontrados, por exemplo, aqui.

  • linhas 17-18: adição dessa barra de abas à barra de aplicativos presente na visualização XML [activity_main];
  • linhas 20-47: gerenciador de eventos da barra de abas;
  • linhas 22-36: apenas o evento [onTabSelected] é gerenciado. Ele corresponde a um clique na aba [Tab tab] passada como parâmetro para o método ou a uma operação de software [TabLayout.Tab.select];
  • linha 30: posição da aba selecionada;
  • linha 32: essa posição é armazenada na sessão;
  • linha 34: trata-se agora de exibir o fragmento associado a essa aba. Apenas a classe filha (linha 52) pode fazer essa associação. Observe-se que não se associa a barra de abas ao contêiner de fragmentos [mViewPager], como foi feito em alguns exemplos estudados. Aqui, separamos totalmente o gerenciamento da barra de abas do gerenciamento dos fragmentos. É por isso que, quando uma aba é clicada, somos obrigados a indicar qual visualização queremos que seja exibida;
  • linha 28: distingue-se a seleção de aba com ou sem navegação. Em geral, quando o usuário clica em uma aba, deseja-se que haja navegação, e durante uma seleção programada, não se deseja isso. Cabe ao desenvolvedor distinguir esses dois casos com o elemento [session.navigationOnTabSelectionNeeded]. Quando a navegação não é realizada, o número da última aba selecionada não é registrado na sessão. Caberá ao desenvolvedor fazer isso;

2.5.7.8. O gerenciador de abas [CustomTabLayout]

  

Utilizamos um gerenciador de abas personalizado para poder exibir o título das abas com diferentes fontes. A classe [CustomTabLayout] é a seguinte:


package client.android.architecture.custom;

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

public class CustomTabLayout extends TabLayout {
  private Typeface mTypeface;

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

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

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

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

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

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

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

}
  • a personalização da fonte dos títulos das abas é feita nas linhas 30 e 44;

O arquivo [fonts] é o seguinte:

  

Fontes:

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

2.5.7.9. Últimas inicializações


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // pai
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // instanciação do gerenciador de fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // o contêiner de fragmentos está associado ao gerenciador de fragmentos
    // ou seja, o fragmento nº i do contêiner de fragmentos é o fragmento nº i fornecido pelo gerenciador de fragmentos
    mViewPager = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // desativa-se o deslize entre fragmentos
    mViewPager.setSwipeEnabled(false);
    // adjacência dos fragmentos
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    // exibe-se a primeira visualização
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    // passa o controle para a atividade filha
    onCreateActivity();
  }
...
  // classes filhas
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • linhas 10-19: aqui encontramos um código frequentemente encontrado nos exemplos estudados;
  • linhas 21-23: exibição da primeira visualização. Sem dúvida, há várias maneiras de distinguir esse caso. Aqui, utilizamos o fato de que, para a primeira visualização, o valor da ação que provoca a mudança de visualização é NONE;
  • linha 22: não fazemos nenhuma suposição sobre o primeiro fragmento a ser exibido. Em nossos exemplos, esse fragmento costumava ser o nº 0, mas nem sempre (ver Exemplo-22). Portanto, solicitaremos à atividade filha (linha 30) que nos informe qual é essa primeira visualização;
  • linha 25: aqui, fatoramos tudo o que foi possível. Agora, a classe filha precisa realizar suas próprias inicializações (linha 29);

2.5.7.10. Gerenciamento da imagem de espera

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


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

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

2.5.7.11. Implementação da interface [IDao]

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


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

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

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

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

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

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

2.5.7.12. Implementação do gerenciador de fragmentos

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


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

    private AbstractFragment[] fragments;

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

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

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

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

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

  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • linha 5: a matriz dos fragmentos associados à atividade. Todos os fragmentos serão derivados da classe [AbstractFragment];
  • linhas 8-12: este é o construtor que inicializa a matriz de fragmentos. Ele solicita esses fragmentos à classe filha da atividade (linha 35);
  • linhas 28-31: os títulos dos fragmentos podem ser usados em um aplicativo onde há tantas abas quanto fragmentos. Nesse caso, é possível atribuir à aba o título do fragmento. Aqui, esses títulos são solicitados à classe filha (linha 37);

2.5.7.13. O método [onResume]

O método [onResume] é executado um pouco antes de a visualização associada à atividade se tornar visível. Ele é utilizado aqui para selecionar uma aba após um salvamento/restauração:


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

2.5.7.14. Résumé

A classe abstrata [AbstractActivity] será a classe pai da única atividade do aplicativo.

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


  // classes filhas
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

protected abstract int getFirstView();

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


  // a sessão
  protected ISession session;
  // o contêiner de fragmentos
  protected MyPager mViewPager;
  // barra de abas
  protected CustomTabLayout tabLayout;
  // nome da classe
protected String className;

2.5.8. A atividade [MainActivity]

  

A classe [MainActivity] pode ter um nome diferente. Sua única restrição é implementar a interface [IMainActivity]. A classe básica fornecida é a seguinte:


package client.android.activity;

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

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

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

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

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

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


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

  @Override
  protected void navigateOnTabSelected(int position) {
    // tarefa: navegação por abas — definir a visualização a ser exibida
  }

  @Override
  protected int getFirstView() {
    // tarefa: navegação por abas — definir a primeira visualização a ser exibida
    return 0;
  }
}
  • linha 14: para que a notação AA [@Bean] da linha 19 seja compreendida, é necessário que a atividade tenha a notação AA [@EActivity];
  • linha 15: a atividade está associada ao menu XML [menu_main]. Atualmente, esse menu está vazio. O desenvolvedor deverá preenchê-lo, se necessário;
  • linha 16: a classe estende a classe [AbstractActivity];
  • linhas 19-20: uma referência à camada [DAO]. Ela será instanciada pela biblioteca AA antes que este campo seja inicializado. Isso implica que o bean AA [Dao] deve existir. Esse é sempre o caso com o esqueleto de aplicativo que fornecemos. Mesmo em um aplicativo sem a camada [DAO], é possível deixar o pacote [dao] existir. Isso não causa complicações;
  • linha 22: a sessão como instância do tipo [Session]. A sessão existe na classe pai [AbstractActivity], mas como instância da interface [ISession] (linha 32);
  • linhas 24-63: os seis métodos impostos pela classe pai [AbstractActivity];
  • linhas 36-39: o método [getDao] retorna uma referência à camada [DAO]. Aqui, essa referência nunca é null. No entanto, na classe pai [AbstractActivity], foi previsto o caso em que a classe filha retornasse uma referência null para indicar que não havia uma camada [DAO]. Se quisermos utilizar essa possibilidade (que, na minha opinião, não é muito útil), é aqui que devemos retornar o ponteiro null;

2.6. A camada [DAO]

Image

  

2.6.1. A interface IDao

Ela foi apresentada no parágrafo 2.5.5:


package client.android.dao.service;

import rx.Observable;

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

  // usuário
  void setUser(String user, String mdp);

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

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

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

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

  // tarefa: declare sua interface aqui
}

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

2.6.2. A interface [WebClient]

  

A interface [WebClient] é a seguinte:


package client.android.dao.service;

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

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

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // tarefa: declare aqui os URL a serem alcançados
}

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

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

  

A classe [MyAuthInterceptor] é a seguinte:


package client.android.dao.service;

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

import java.io.IOException;

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

  // usuário
  private String user;
  // senha
  private String mdp;

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

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

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

Authorization: Basic code

onde [code] é o código Base64 da sequência 'user:mp'. Essa classe só é utilizada se o servidor jSON esperar por essa forma de autenticação. Existem outras.

Observação: o uso desta classe é ilustrado no parágrafo 3.6.3.1.

2.6.4. A classe [AbstractDao]

  

A classe [AbstractDao] é a seguinte:


package client.android.dao.service;

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

public abstract class AbstractDao {

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

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

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

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

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

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

2.6.5. A classe [Dao]

  

A classe [Dao] é a seguinte:


package client.android.dao.service;

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

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

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

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

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

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

  @Override
  public void setUser(String user, String mdp) {
    // registra-se o usuário no interceptador
    authInterceptor.setUser(user, mdp);
  }

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

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

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

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

2.7. Os fragmentos

  

2.7.1. A classe [MenuItemState]

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


package client.android.architecture;

public class MenuItemState {

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

  // construtores
  public MenuItemState() {

  }

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

  // getters e setters
...
}

2.7.2. A classe [Utils]

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


package client.android.architecture;

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

public class Utils {

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

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

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

2.7.3. A classe pai [AbstractFragment]

A classe [AbstractFragment] reúne o que é comum a todos os fragmentos do aplicativo. Assim como na classe [AbstractActivity], seu código é complexo. Também vamos analisá-lo passo a passo.

2.7.3.1. A estrutura


package client.android.architecture.core;

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

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

public abstract class AbstractFragment extends Fragment {

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

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


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

  // gerenciamento do menu ------------------------------------------
  ...

  // gerenciamento de espera -------------------------------------------------------------
...

  // gerenciamento de operações assíncronas --------------------------------------------------------------------
...

  // gerenciamento de exceções -------------------------------------------------------------------
....

  // gerenciamento do ciclo de vida do fragmento --------------------------------------------------------
...

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

  protected abstract int getNumView();

  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();

  protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);

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

2.7.3.2. O construtor

O construtor da classe é o seguinte:


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

2.7.3.3. Gerenciamento do menu

Em nossa arquitetura, todo fragmento deve ter um menu, mesmo que vazio. Os logs mostraram, de fato, que quando o método [onCreateOptionsMenu] — executado quando o fragmento possui um menu — é executado, o fragmento já foi associado à sua atividade, à sua visualização e ao seu menu e se tornará visível. Portanto, esse é o momento em que a atualização da interface visual e do menu pode ser realizada. É nesse método [onCreateOptionsMenu] que solicitamos que o fragmento filho seja atualizado.

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


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

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

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

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

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

2.7.3.4. Gerenciamento da espera pelo término de uma tarefa assíncrona


   // assinaturas de observáveis
  private List<Subscription> abonnements = new ArrayList<>();
// tarefas assíncronas
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...

  // gerenciamento da espera pelo término de uma operação assíncrona -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // registra-se o número de tarefas que serão executadas
    this.numberOfRunningTasks = numberOfRunningTasks;
    // insere-se a imagem de espera
    mainActivity.beginWaiting();
    // esvazia-se a lista de assinaturas
    abonnements.clear();
    // ainda não houve cancelamento
    runningTasksHaveBeenCanceled = false;
  }

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

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

2.7.3.5. Gerenciamento de exceções


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

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

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

2.7.3.6. Gerenciamento de operações assíncronas


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

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

  private void endOfTask() {
...
  }

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

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

O método [endOfTask] é o seguinte:


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

O método [consumeThrowable] é o seguinte:


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

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

...
  // classes filhas -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • linha 3: o método [consumeThrowable] recebe a exceção que ocorreu;
  • linha 15: todas as tarefas ainda ativas são canceladas;
  • linha 17: exibe-se o texto da exceção;
  • linhas 21-37: cancelamento de todas as tarefas;
  • linhas 27-29: todas as assinaturas são canceladas;
  • linha 31: registra-se que houve cancelamento;
  • linha 32: o contador de tarefas é zerado;
  • linha 34: a espera é cancelada;
  • linha 36: notifica-se o fragmento filho sobre o término das tarefas após o cancelamento;

2.7.3.7. Gerenciamento do ciclo de vida do fragmento


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

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

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

  private void saveState() {
...
  }

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


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

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


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

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

  private void saveState() {
...
  }

  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
    }
    // pai
    super.onSaveInstanceState(outState);
    // salvar o fragmento somente se ele estiver visível
    if (isVisibleToUser) {
      // talvez o backup já tenha sido feito
      if (!saveFragmentDone) {
        saveState();
      }
      // restauração a ser feita em todos os casos
      session.setAction(ISession.Action.RESTORE);
    }
}
  • linhas 6-19: o fragmento é salvo se ele passar do estado exibido para o estado oculto (linha 11). É o método [setUserVisibleHint] que nos fornece essa informação;
  • linha 14: o salvamento é feito pelo método privado das linhas 21-23;
  • linhas 25-41: durante uma rotação do dispositivo, o método [onSaveInstanceState] será chamado. O fragmento é salvo sob duas condições:
    • ele esteja visível (linha 34);
    • ainda não tenha sido salvo (linha 36). É possível que os métodos [setUserVisibleHint, onSaveInstanceState] não possam ser executados ambos quando o fragmento estiver visível e que, portanto, o gerenciamento do booleano [saveFragmentDone] seja desnecessário. Por precaução, optei por utilizá-lo;
  • linha 40: após o salvamento, virá a restauração. Observe que, na próxima vez que o fragmento precisar ser atualizado, isso deverá ser feito por meio de uma operação [RESTORE];

Observe-se os dois momentos em que é solicitado um salvamento do fragmento:

  1. quando ele 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 serem canceladas?
    if (numberOfRunningTasks != 0) {
      // cancelamos as tarefas
      cancelRunningTasks();
    }
    // salvamos o estado do fragmento
    CoreState currentState = saveFragment();
    // o fragmento foi visitado
    currentState.setHasBeenVisited(true);
    // salvar o estado do menu
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // início da sessão
    session.setCoreState(getNumView(), currentState);
    // salvamento concluído
    saveFragmentDone = true;
    // registro
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }


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

protected abstract int getNumView();
  • linhas 4-7: a rotação do dispositivo pode ocorrer enquanto operações assíncronas estão em andamento. Nesse caso, decide-se cancelar todas elas. Essa não é uma boa decisão para o usuário, que terá de refazer uma nova solicitação, possivelmente demorada, quando na verdade apenas moveu seu celular ou tablet ou recebeu uma ligação. É possível manter as conexões de rede durante um ciclo de backup/restauração. No entanto, as soluções não são óbvias e decidi não abordá-las neste curso para iniciantes. O caminho a seguir é estabelecer essas conexões de rede por meio de um fragmento sem interface visual associada e que não seja destruído durante o ciclo de backup/restauração. Para isso, basta usar a instrução [Fragment.setRetainInstance(true)];
  • linha 9: solicita-se ao fragmento filho que salve seu estado em um tipo derivado de [CoreState] (linha 31);
  • linha 11: registra-se que o fragmento foi visitado. Essa informação é útil. Quando um fragmento é visitado pela primeira vez, sua atualização pode ser diferente das seguintes, pois, nesse caso, ele não possui um estado anterior na sessão;
  • linha 13: salvamos o estado do menu, o que nos permitirá restaurá-lo automaticamente;
  • linha 15: esse estado atual é salvo na sessão. Nela, os estados são agrupados por visualização/fragmento, sendo que cada um deles possui um estado. O número da visualização é fornecido pelo fragmento filho (linha 33);
  • linha 17: observa-se que o fragmento foi salvo. Isso porque dois métodos podem chamar o método [saveState] e não faz sentido realizar dois salvamentos;

A regeneração da visualização associada ao fragmento é garantida pelo seguinte método:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // pai
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // a visualização deve ser restaurada
    viewHasToBeInitialized = true;
}

No ciclo de vida, o método [onActivityCreated] é executado logo após o método [onCreateView]. A chamada desse último método indica que a visualização associada ao fragmento deve ser reconstruída. Basta observar isso na linha 10.

2.7.3.8. Atualização do fragmento

A atualização do fragmento é a última operação realizada no fragmento antes que ele se torne visível e fique aguardando as ações do usuário. Ela é realizada pelo código a seguir:


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

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

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

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

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

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

etapa 1


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


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

  protected abstract void initFragment(CoreState previousState);

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

etapas 2 e 3


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


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

...

  protected abstract void initFragment(CoreState previousState);

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

etapa 4


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


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

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

protected abstract void updateOnRestore(CoreState previousState);
  • linhas 34-66: processa-se a ação em andamento, que pode ser uma das três seguintes:
    • RESTORE: estamos restaurando o fragmento após uma rotação do dispositivo;
    • NAVIGATION: retorna-se ao fragmento com o objetivo de encontrá-lo no estado em que foi deixado na última vez em que foi utilizado;
    • SUBMIT: todos os outros casos;
  • linha 34: recupera-se a ação em andamento;
  • linhas 36-42: para uma ação do tipo SUBMIT, chama-se o método [updateOnSubmit] do fragmento filho (linhas 41, 68), passando-lhe o último estado conhecido do fragmento;
  • linhas 43-55: para uma ação do tipo NAVIGATION;
  • linhas 47-54: queremos restaurar o fragmento ao seu último estado conhecido. A operação NAVIGATION pode ocorrer em uma primeira visita. Esse seria o caso, por exemplo, em um aplicativo com abas: se eu passar da aba 1 para a aba 4:
    • preciso inicializar o fragmento da aba 4 se for a primeira visita;
    • restaurar o fragmento da aba 4 ao seu estado anterior, caso não seja a primeira visita;
  • linhas 52-54: não se faz nada se for a primeira visita. Caberá ao método filho [initView(CoreState previousState)] realizar essa inicialização. A primeira visita é caracterizada pela condição [previousState==null];
  • linha 49: se não for a primeira visita ao fragmento, restaura-se o menu dele;
  • linha 51: solicita-se que a classe filha seja atualizada chamando o método da linha 70. Passa-se a ela o estado anterior do fragmento para que ela possa realizar seu trabalho;
  • linhas 56-66: no caso de uma operação de restauração do fragmento, faz-se o mesmo que no caso de uma navegação fora da primeira visita;

etapa 5


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


    // etapa 5: atualizações finais ----------------------
    // mudamos de visualização
    session.setPreviousView(getNumView());
    // não há mais nenhuma ação em andamento
    session.setAction(ISession.Action.NONE);
    // ao sair deste fragmento, ele deverá ser salvo
    saveFragmentDone = false;
    // enquanto o fragmento não for reconstruído, ele não precisa ser inicializado
    fragmentHasToBeInitialized = false;
    // enquanto a visualização não for reconstruída, ela não precisa ser inicializada
    viewHasToBeInitialized = false;
    // retornamos ao funcionamento normal da seleção de abas
    session.setNavigationOnTabSelectionNeeded(true);

    // é sinalizado ao fragmento que a visualização está pronta
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • linhas 18-30: ao chegar aqui, o fragmento já foi inicializado e está pronto para ser exibido. Em seguida, todos os indicadores utilizados no gerenciamento do ciclo de vida do fragmento são repostos em seu estado inicial;
  • linha 20: mudamos de visualização: registramos isso na sessão;
  • linha 22: não há mais nenhuma ação em andamento;
  • linha 24: ao sair do fragmento agora exibido, será necessário salvá-lo ao sair;
  • linha 26: o fragmento não precisa mais ser reconstruído. Esse indicador será redefinido para vrai quando o construtor do fragmento for executado novamente;
  • linha 28: a visualização associada ao fragmento não precisa mais ser inicializada. Esse indicador será redefinido para vrai quando o método [onActivityCreated] for executado novamente;
  • linha 30: o fragmento pode ser exibido em um aplicativo com abas. Nesse caso, quando o usuário clicar em uma delas, deve ocorrer uma troca de fragmento;
  • linha 36: indica-se à classe filha que o fragmento está pronto. Ela pode incluir no método [notifyEndOfUpdates] as atualizações que devem ser feitas em todos os casos, iniciar uma operação assíncrona para obter novos dados, etc.

2.7.4. Um exemplo de fragmento

  

Incluímos no projeto [client-android-skel] um exemplo de fragmento para mostrar ao leitor a estrutura típica de um fragmento de um aplicativo baseado nesse projeto.

A classe [DummyFragment] é a seguinte:


package client.android.fragments.behavior;

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

public class DummyFragment extends AbstractFragment {

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

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

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

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

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

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

  @Override
  protected int getNumView() {
    // é necessário retornar o número do fragmento na tabela de fragmentos gerenciados pela atividade (cf. MainActivity)
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // o fragmento fica visível e passou por uma construção nesta etapa ou em uma etapa anterior
    // isso ocorre ao iniciar o aplicativo e a cada rotação do dispositivo Android
    // é necessariamente seguida pela execução de [initView]
    // é necessário inicializar os campos do fragmento que foi reconstruído
    // previousState é o último registro do fragmento — vale null se for a primeira visita ao fragmento
  }

  @Override
  protected void initView(CoreState previousState) {
    // o fragmento se torna visível e a visualização associada foi reconstruída nesta etapa ou em uma etapa anterior
    // isso ocorre sempre que [initFragment] é executado e sempre que o fragmento sai da adjacência do fragmento exibido
    // é necessário inicializar os componentes da visualização que foi reconstruída
    // previousState é o último salvamento do fragmento — vale null se for a primeira visita ao fragmento

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // é executado após [initFragment, initView] se esses métodos forem executados
    // a visualização será exibida após uma operação do tipo SUBMIT
    // geralmente é necessário inicializar o fragmento e a visualização associada a partir da sessão
    // previousState é o último salvamento do fragmento — vale null se for a primeira visita ao fragmento
    // não há nada a fazer se não for possível acessar o fragmento por meio de uma operação SUBMIT
    // se for possível chegar ao fragmento por meio de operações SUBMIT a partir de fragmentos diferentes, é possível conhecer a visualização anterior por meio de [session.getPreviousView]
    // se for possível chegar ao fragmento por meio de várias operações SUBMIT a partir do mesmo fragmento, então é necessário definir um indicador na sessão para diferenciar os diferentes tipos de SUBMIT a partir desse fragmento
  }

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

  }

  @Override
  protected void notifyEndOfUpdates() {
    // ocorre após os métodos [updateOnSubmit, updateOnRestore]
    // neste ponto, a visualização já foi construída e inicializada
    // geralmente não há nada a fazer aqui, mas também é possível agrupar aqui as ações que precisariam ser realizadas independentemente da forma como se chega a essa visualização
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // chamada quando as tarefas assíncronas iniciadas pelo fragmento são concluídas ou canceladas
    // esses dois casos podem ser diferenciados graças ao parâmetro runningTasksHaveBeenCanceled
    // geralmente, é necessário redefinir a visualização para um estado diferente daquele em que se encontrava enquanto aguardava as respostas das tarefas assíncronas

  }
}

A classe [DummyFragment] pode não ter um estado. Aqui, incluímos um para lembrar o que é esperado nela:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

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

Para ilustrar o uso do projeto [client-android-skel], vamos primeiro utilizar exemplos simples antes de passarmos para um estudo de caso mais completo.

2.8. Exercícios ilustrativos

Começaremos refatorando exemplos já escritos.

2.8.1. Exemplo-17B

Retomamos o exemplo 17 estudado no parágrafo 1.18. Trata-se de um aplicativo com um único fragmento, sem tarefas assíncronas e sem abas. Vamos examiná-lo para ver como ele se comporta durante uma rotação do dispositivo. Fazemos as seguintes entradas:

Image

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

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], aparece uma caixa de diálogo mostrando os dados inseridos no formulário. Se, nesse momento, girarmos o dispositivo, a caixa de diálogo desaparece.

Portanto, durante uma rotação, precisaremos regenerar:

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

2.8.1.1. O projeto [Exemple-17B]

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

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

A margem superior da visualização [vue1.xml] será alterada para 80 dp:


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

Nesta fase, pode-se tentar uma primeira compilação para verificar os erros. Os primeiros erros sinalizados provêm do imports, devido a pacotes que mudaram de local. Corrige-se esses erros (Ctrl-Shift-O). Outros erros decorrem do fato de que a vista [Vue1Fragment] não implementa todos os métodos exigidos por sua classe pai [AbstractParent]:

Image

Geramos os métodos que faltam (Alt-Enter).

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

Image

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

 

Nesta fase, é possível recompilar para verificar os erros restantes. O único erro relatado está no método [Vue1Fragment.updateFragment]:

 

É preciso remover a anotação [@Override] da linha 135. Agora não há mais erros. Vamos partir daqui para modificar o projeto.

2.8.1.2. O estado do fragmento [Vue1Fragment]

O fragmento [Vue1Fragment] precisa salvar informações durante a rotação do dispositivo para que possa ser restaurado completamente. Criamos uma classe [Vue1FragmentState] para isso:

  

Por enquanto, essa classe está vazia:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {
  
}

2.8.1.3. Personalização do projeto

  

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

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


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

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

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

  // gerenciamento de espera
  void beginWaiting();

  void cancelWaiting();

  // constantes do aplicativo -------------------------------------

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

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

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

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

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

  // barra de abas
  boolean ARE_TABS_NEEDED = false;

  // imagem de espera
  boolean IS_WAITING_ICON_NEEDED = false;

  // número de fragmentos do aplicativo
  int FRAGMENTS_COUNT = 1;

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

A classe [Session] é a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

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

}

Ela está vazia. De fato, como há apenas um fragmento, não há necessidade de prever uma comunicação entre fragmentos com uma sessão.

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


package client.android.architecture.custom;

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

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

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

2.8.1.4. A atividade [MainActivity]

A atividade [MainActivity] está atualmente da seguinte forma:


package client.android.activity;

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

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

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

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

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

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


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

  @Override
  protected void navigateOnTabSelected(int position) {
    // tarefa: navegação por abas — definir a visualização a ser exibida quando a aba nº [position] for selecionada
  }

  @Override
  protected int getFirstView() {
    // tarefa: definir o número da primeira visualização (fragmento) a ser exibida
    return 0;
  }
}

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


package client.android.activity;

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

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

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

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

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

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

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

  @Override
  protected void navigateOnTabSelected(int position) {

  }

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

Apenas o método das linhas 41 a 44 deve ser alterado. Ele deve retornar a matriz de fragmentos do aplicativo. Na linha 43, não se esqueça de colocar o sublinhado após o nome do fragmento.

2.8.1.5. O estado do fragmento [FragmentState]

Após os testes de rotação realizados no projeto [Exemple-17], decidiu-se armazenar os seguintes elementos do fragmento:

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

A classe [Vue1FragmentState] será a seguinte:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

import java.util.List;

public class Vue1FragmentState extends CoreState {

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

  // getters e setters
...
}

2.8.1.6. O fragmento [AbstractFragment]

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


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

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

      public void onStopTrackingTouch(SeekBar seekBar) {
      }

      public void onStartTrackingTouch(SeekBar seekBar) {
      }

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

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


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

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

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

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

      public void onStopTrackingTouch(SeekBar seekBar) {
      }

      public void onStartTrackingTouch(SeekBar seekBar) {
      }

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

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

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

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

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

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


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

  private void doAfficher(final List<String> messages) {
    // construímos o texto a ser exibido
    StringBuilder texte = new StringBuilder();
    for (String message : messages) {
      texte.append(String.format("%s\n", message));
    }
    // armazenamento da mensagem
    message = texte.toString();
    // exibição
    showMessage();
  }

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

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

  • linhas 14-20: a lista de mensagens é concatenada em uma única mensagem, que é armazenada na linha 2;
  • linhas 25-33: é essa mensagem que a caixa de diálogo exibe e é essa mesma mensagem que o método [updateOnRestore] faz com que seja exibida;
  • linha 27: o segundo parâmetro do método [setNeutralButton] é o método executado quando o usuário clica no botão [Fermer] da caixa de diálogo;
  • linha 31: ao fechar a caixa de diálogo, a mensagem é repassada para null para indicar que a caixa de diálogo não está mais presente;

2.8.1.7. Tests

Sugere-se ao leitor que teste este projeto e verifique se o fragmento é mantido após uma ou mais rotações sucessivas.

2.8.2. Exemplo 23: cliente de previsão do tempo

Alguns sites disponibilizam informações meteorológicas na forma de sequências como jSON. Veja um exemplo:

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 do tempo se deseja, neste caso Angers;
  • country: o país da cidade, neste caso a França (fr);
  • APPID: uma chave obtida ao se cadastrar no site [https://home.openweathermap.org/users/sign_up];

2.8.2.1. O projeto

  

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

  • possui apenas um fragmento cujo estado não precisa ser mantido;
  • ele realiza consultas assíncronas;

2.8.2.2. Personalização do projeto

  

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


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

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

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

  // gerenciamento da fila de espera
  void beginWaiting();

  void cancelWaiting();

  // constantes do aplicativo -------------------------------------

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

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

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

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

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

  // barra de abas
  boolean ARE_TABS_NEEDED = false;

  // imagem de espera
  boolean IS_WAITING_ICON_NEEDED = true;

  // número de fragmentos do aplicativo
  int FRAGMENTS_COUNT = 1;

}
  • linhas 25, 28, 31, 40: características da camada [DAO]. Linha 31: não há necessidade de autenticação básica;
  • linha 34: adjacência dos fragmentos. Aqui, essa constante não tem importância, pois há apenas um fragmento;
  • linha 37: não se trata de um aplicativo com abas;
  • linha 43: há apenas um fragmento;

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


package client.android.architecture.custom;

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

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

  // getters e setters
...
}
  • linhas 10-13: não há nada a declarar, pois, neste aplicativo, há apenas um fragmento cujo estado não é mantido;

A classe [Session] é a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

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

Ela está vazia, pois, nesta aplicação, não há comunicação entre fragmentos.

2.8.2.3. A camada [DAO]

  

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

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

A interface [WebClient] será a seguinte:


package client.android.dao.service;

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

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

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

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

A interface [IDao] será a seguinte:


package client.android.dao.service;

import rx.Observable;

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

  // usuário
  void setUser(String user, String mdp);

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

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

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

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

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

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


package client.android.dao.service;

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

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

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

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

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

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

  @Override
  public void setUser(String user, String mdp) {
    // registra-se o usuário no interceptador
    authInterceptor.setUser(user, mdp);
  }

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

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


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

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

2.8.2.4. A atividade [MainActivity]

  

A atividade [MainActivity] é a seguinte:


package client.android.activity;

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

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

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

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

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

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


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

  @Override
  protected void navigateOnTabSelected(int position) {
  }

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

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

2.8.2.5. O fragmento [MeteoFragment]

  

O fragmento [MeteoFragment] consulta o serviço web / jSON de previsão do tempo. Sua estrutura é a seguinte:


package client.android.fragments;

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

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

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

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

A visualização exibe apenas o texto da linha 10;

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

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

O código completo do fragmento é o seguinte:


package client.android.fragments.behavior;

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

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

  // dados locais
  private int nbReponsesRecues;

  // gerenciamento de eventos ---------------------------------------------------------------------------------------
  // cidades cuja previsão do tempo se deseja
  final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

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

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

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

  }

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

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

  // gestão do ciclo de vida ---------------------------------------------------------------------------------------
...
}
  • linhas 25-50: tratamento do clique na opção de menu [Météo];
  • linha 32: construção do URL do serviço web / jSON do serviço meteorológico. Em seguida, ela é passada para a camada [DAO] por meio da atividade;
  • linha 34: iniciamos a espera. Passamos o número de tarefas que serão iniciadas, para que a classe pai possa nos notificar quando elas forem concluídas. Aqui, há cinco tarefas, pois vamos solicitar a previsão do tempo das cinco cidades da linha 23;
  • linha 16: vamos contar o número de respostas recebidas para poder exibi-lo;
  • linhas 38-50: percorremos as cidades cuja previsão do tempo queremos obter;
  • linha 40: vamos fazer 5 solicitações HTTP em paralelo;
  • linha 40: solicitamos à classe pai [AbstractParent] que consulte o serviço web /jSON;
  • linhas 40-48: o método [executeInBackground] espera dois parâmetros:
    • linha 40: o processo a ser observado e executado é fornecido pelo método [mainActivity.getWeatherForecast];
    • linhas 40-48: a instância [Action1] a ser executada ao receber a resposta do serviço assíncrono. O tipo T de [Action1<T>] deve ser o tipo T do resultado do método [getWeatherForecast];
  • linha 44: foi recebida uma resposta. Ela é passada para o método [consumeResponse] da linha 53;
  • linha 46: o contador de respostas recebidas é incrementado;
  • linhas 53-56: processamento de uma resposta jSON do serviço meteorológico;
  • linha 55: apenas se registra a string jSON;
  • linhas 59-72: código executado antes do lançamento das tarefas assíncronas;
  • linha 65: passa-se o número de tarefas a serem executadas para a classe pai [AbstractParent]. É isso que permite que ela nos avise quando todas estiverem concluídas;
  • linhas 67-70: preparação do menu para uma espera. Mantemos apenas a opção [Actions/Annuler], que permitirá ao usuário cancelar as tarefas iniciadas;
  • linhas 74-92: código executado quando a classe pai nos avisa que todas as tarefas iniciadas foram concluídas;
  • linha 77: o menu é restaurado ao seu estado inicial. O método [initMenu] (linhas 95-102) exibe o menu com todas as suas opções, exceto a opção [Actions/Annuler], que fica oculta;
  • linhas 80-91: exibe-se o número de respostas recebidas;

O clique na opção de menu [Annuler] é processado pelo código a seguir:


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

O clique na opção de menu [Terminer] é gerenciado pelo código a seguir:


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

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


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

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

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

  @Override
  protected void initFragment(CoreState previousState) {

  }

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


  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

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

  // cidades cuja previsão do tempo se deseja
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

mas ele se inicializa sozinho;

  • linhas 18-24: servem para inicializar a visualização associada ao fragmento assim que ela for construída (previousState==null) ou reconstruída (previousState!=null);
  • linhas 21-23: se for a primeira visita ao fragmento, inicializa-se seu menu para ocultar a opção [Annuler];
  • linhas 27-30: chamadas se, para chegar ao fragmento, houve navegação com uma ação do tipo [SUBMIT]. Aqui, não há navegação entre fragmentos, pois há apenas um fragmento;
  • linhas 32-35: chamadas durante um ciclo de salvamento/restauração devido a uma rotação do dispositivo ou outro motivo. Aqui, como não foi salvo nenhum estado, não há nada a ser feito;
  • linhas 37-40: chamadas quando todas as atualizações anteriores foram realizadas. Aqui, não há nada a ser feito;

2.8.2.6. Tests

Agora vamos executar o exemplo:

Image

Image

Os logs ficam assim:


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

Agora, fazemos a consulta com um identificador API incorreto:


    String APIID = "";

Image

Os logs ficam então assim:


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

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


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

2.8.3. Exemplo 16B

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

Image

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

Image

Percebe-se que todas as mensagens de erro foram perdidas. Vamos tentar melhorar isso.

2.8.3.1. O projeto Exemplo-16B

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

  

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

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

Alteraremos a margem superior da visualização [vue1.xml] para 80 dp:


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

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

  • um primeiro tipo de erros é o relacionado a imports. Algumas classes mudaram de pacote na migração para [Exemple-16B]. Começamos corrigindo esse tipo de erro;
  • um segundo tipo de erro é sinalizado na classe [Vue1Fragment] porque ela não implementa os métodos exigidos pela classe pai [AbstractParent]. Fazemos uma geração automática desses métodos;

Tentamos uma segunda compilação:

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

2.8.3.2. Criação de um relatório para o fragmento [Vue1Fragment]

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

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {

}

2.8.3.3. Personalização do projeto

  

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


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

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

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

  // gerenciamento da fila de espera
  void beginWaiting();

  void cancelWaiting();

  // constantes do aplicativo -------------------------------------

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

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

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

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

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

  // barra de abas
  boolean ARE_TABS_NEEDED = false;

  // imagem de espera
  boolean IS_WAITING_ICON_NEEDED = true;

  // número de fragmentos do aplicativo
  int FRAGMENTS_COUNT = 1;

}
  • linhas 25, 28, 31, 40: características da camada [DAO]. Não há necessidade de autenticação básica;
  • linha 34: adjacência dos fragmentos. Aqui, essa constante não tem importância, pois há apenas um fragmento;
  • linha 37: não se trata de um aplicativo com abas;
  • linha 43: há apenas um fragmento;

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


package client.android.architecture.custom;

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

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

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

A classe [Session] é a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

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

Ela está vazia, pois, nesta aplicação, não há comunicação entre fragmentos.

2.8.3.4. A camada [DAO]

  

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

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

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


package client.android.dao.service;

import java.util.List;

public class Response<T> {

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

    // construtores
    public Response() {

    }

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

    // getters e setters
...
}

A interface [WebClient] será a seguinte:


package client.android.dao.service;

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

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

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

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

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

A interface [IDao] será a seguinte:


package client.android.dao.service;

import rx.Observable;

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

  // usuário
  void setUser(String user, String mdp);

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

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

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

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

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

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

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


package client.android.dao.service;

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

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

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

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

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

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

  @Override
  public void setUser(String user, String mdp) {
    // registra-se o usuário no interceptador
    authInterceptor.setUser(user, mdp);
  }

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

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

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

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

}
  • vale lembrar que as linhas 17 a 85 estão presentes por padrão na classe [Dao] do projeto [client-android-skel]. Basta adicionar os métodos de implementação da interface [IDao];
  • linhas 88 a 97: implementação do método [getAlea]. Essa implementação é muito simples e ocupa 6 linhas, das linhas 91 a 96;
  • linha 91: o método [getResponse] é um método da classe pai [AbstractDao]. Ele espera um parâmetro do tipo [IRequest<T>], em que T é o tipo da resposta esperada, neste caso, um tipo Response<Integer>. O tipo T de [IRequest<T>] (linha 91) deve ser o tipo T do método [Observable<T> getAlea] (linha 89);
  • A interface [IRequest<T>] possui apenas um método: getResponse. Esse método tem a função de fornecer a resposta do tipo T que deve ser retornada pelo método [Observable<T> getAlea];
  • linha 94: é a interface [WebClient] que fornece essa resposta. São passados a ela os dois parâmetros recebidos na linha 89. Por esse motivo, esses parâmetros devem ter o atributo final;

2.8.3.5. A atividade [MainActivity]

  

A atividade [MainActivity] é a seguinte:


package client.android.activity;

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

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

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

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

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

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


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

  @Override
  protected void navigateOnTabSelected(int position) {
    // navegação por abas — definir a visualização a ser exibida
  }

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

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

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

2.8.3.6. O estado do fragmento [Vue1Fragment]

  

A classe [Vue1FragmentState] será a seguinte:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

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

public class Vue1FragmentState extends CoreState {

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

  // getters e setters
...
}

Para determinar o que precisava ser armazenado no fragmento, realizamos rotações do dispositivo em diversas situações e observamos o que havia desaparecido durante a restauração. Chegamos à conclusão de que era necessário armazenar as informações das linhas 10 a 23.

2.8.3.7. O fragmento [Vue1Fragment]

  

Atualmente, a visualização [Vue1Fragment] apresenta diversos erros devido ao fato de que a classe pai [AbstractFragment], da qual ela deriva, foi alterada. Em vez de descrever uma a uma as alterações a serem feitas, vamos comentar diretamente a versão final.

A estrutura do fragmento é a seguinte:


package client.android.fragments.behavior;

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

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

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

...
}
  • na linha 26, lembramos que todo fragmento deve ter um menu, mesmo que vazio. É o caso aqui.

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

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

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

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

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

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

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

2.8.3.7.2. O ciclo de vida do fragmento

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


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

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

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

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

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

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

  @Override
  protected void notifyEndOfUpdates() {

  }

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

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

2.8.3.8. Os testes

O leitor é convidado a realizar os seguintes testes:

  • criar erros e acionar o dispositivo: as mensagens de erro devem permanecer exibidas;
  • obter números aleatórios e executar o dispositivo: os números aleatórios obtidos devem permanecer exibidos;
  • definir uma espera de vários segundos e executar o dispositivo durante a espera: as tarefas devem ter sido canceladas (isso pode ser verificado nos logs);

2.8.4. Exemplo-22B

Retomamos aqui o exemplo 22 para refatorá-lo de acordo com o modelo do projeto [client-android-skel]. Vale lembrar que o projeto [Exemple-22] gerencia corretamente o ciclo de salvamento/restauração dos fragmentos durante uma rotação e que foi ele que serviu de base para o projeto [client-android-skel].

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

  

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

Primeiramente, copiamos elementos da pasta [res]:

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

Alteraremos a margem superior das duas visualizações para 120 dp:

[vue1.xml]:


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

[fragment_main]:


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

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

 

Nesta fase, podemos tentar uma primeira compilação. Surge um primeiro tipo de erros: os imports incorretos, pois as classes mudaram de pacote. Corrigimos esses imports. Um segundo tipo de erro se deve ao fato de que os fragmentos não implementam todos os métodos de sua classe pai [AbstractFragment]. Corrigimos pressionando (Alt+Enter).

Os erros restantes decorrem das diferenças existentes entre a classe antiga e a nova [AbstractFragment]. Por enquanto, eles são ignorados.

2.8.4.1. Personalização do projeto

  

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

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


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

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

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

  // gerenciamento da fila de espera
  void beginWaiting();

  void cancelWaiting();

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

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

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

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

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

  // barra de abas
  boolean ARE_TABS_NEEDED = true;

  // imagem de espera
  boolean IS_WAITING_ICON_NEEDED = false;

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

}
  • linhas 23, 26, 29, 38: características da camada [DAO]. Não há nenhuma aqui;
  • linha 41: há aqui cinco fragmentos;
  • linha 32: adjacência dos fragmentos. Essa constante pode assumir aqui um valor em [1,4]. Recomenda-se ao leitor que altere esse valor para verificar se o aplicativo continua funcionando;
  • linha 35: trata-se de um aplicativo com abas;

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


package client.android.architecture.custom;

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

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

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

A classe [Session] é a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

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

  // número de fragmentos visitados
  private int numVisit;
  // Número do fragmento do tipo [PlaceholderFragment] exibido na segunda aba
  private int numFragment = -1;

  // getters e setters
...
}

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

2.8.4.2. A atividade [MainActivity]

  

A atividade [MainActivity] é a seguinte:


package client.android.activity;

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

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

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

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

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

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

Aqui, a classe [MainActivity] é mais extensa do que a dos exemplos anteriores por dois motivos:

  • há abas para gerenciar;
  • há um menu a ser gerenciado;

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

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

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

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


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

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

  @Override
  protected int getFirstView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
  • linhas 2-12: o método [onCreateActivity] é chamado pela classe pai [AbstractActivity] quando a atividade é criada pela primeira vez ou recriada durante um ciclo de salvamento/restauração. Quando esse método é chamado, a classe pai já restaurou a sessão;
  • linha 10: recupera-se uma referência local da sessão. A mudança de tipo se deve ao fato de que a sessão da classe pai é do tipo [AbstractSession];
  • linhas 19-38: o método [getFragments] deve devolver à classe pai a matriz dos fragmentos gerenciados pela aplicação. Aqui, há [FRAGMENTS_COUNT], número definido em [IMainActivity]. Os primeiros fragmentos [FRAGMENTS_COUNT-1] são do tipo [PlaceHolderFragment] e o último, do tipo [Vue1Fragment];
  • linhas 41-45: o método [getFragmentTitle] deve exibir os títulos dos fragmentos quando essa informação for útil. Esse não é o caso aqui;
  • linhas 47-50: esse método é chamado pela classe pai quando o usuário clica em uma aba. Voltaremos a isso no parágrafo seguinte;
  • linhas 52-55: retorna o número da primeira visualização a ser exibida quando o aplicativo é iniciado. Aqui, é o fragmento [Vue1Fragment] que deve ser exibido primeiro. O método [getFirstView] poderia ser substituído, com vantagem, por uma constante em [IMainActivity];

2.8.4.2.2. Gerenciamento de abas

As abas são gerenciadas pelos seguintes métodos:


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

  @Override
  protected void navigateOnTabSelected(int position) {
    // nº do fragmento a ser exibido
    int numFragment;
    switch (position) {
      case 0:
        // nº do fragmento [Vue1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // Número do fragmento [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // exibição do fragmento
    if (numFragment != mViewPager.getCurrentItem()) {
      navigateToView(numFragment, ISession.Action.SUBMIT);
    }
  }
}
  • linhas 1-20: o método [onCreateActivity] é chamado pela classe pai [AbstractActivity] quando a atividade é criada pela primeira vez ou recriada durante um ciclo de salvamento/restauração. Quando esse método é chamado, a classe pai já restaurou a sessão;
  • linha 9: recupera-se uma referência local da sessão. A mudança de tipo se deve ao fato de que a sessão da classe pai é do tipo [AbstractSession];
  • linhas 11-13: cria-se a primeira aba;
  • linhas 15-20: cria-se a segunda aba se um número de fragmento estiver registrado na sessão (linha 15). Esse número é inicialmente igual a -1 na primeira construção da atividade;
  • linhas 23-39: este método é chamado pela classe pai quando o usuário clica em uma aba;
  • linhas 28-31: se a aba 0 for clicada, deve-se exibir [Vue1Fragment]. Sabe-se que essa é a primeira visualização exibida ao iniciar o aplicativo;
  • linhas 32-35: se a aba 1 for clicada, deve-se exibir o fragmento cujo número está registrado na sessão;
  • linhas 37-39: navegamos até o fragmento escolhido. A ação associada é [SUBMIT]. Seria possível que fosse [NAVIGATION]? Neste documento, utiliza-se [NAVIGATION] apenas quando a exibição do novo fragmento requer apenas o conhecimento de seu estado anterior. Aqui, esse não é o caso, pois a exibição do fragmento deve mudar em relação ao seu estado anterior para mostrar mais uma visita;

2.8.4.2.3. Gerenciamento do menu

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


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

que exibe o seguinte:

  

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


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

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // sem navegação ao selecionar uma aba por meio do software
      session.setNavigationOnTabSelectionNeeded(false);
      // recriamos as duas abas devido a um problema com a fonte dos títulos
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
      // o número do fragmento a ser exibido é definido na sessão
      session.setNumFragment(i);
      // seleciona-se a aba nº 2 com navegação
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • linhas 16-31: gerenciamento do clique em uma opção de menu do tipo [Fragmenti];
  • linhas 37-50: exibem o fragmento nº i (trata-se dos fragmentos do tipo PlaceHolderFragment) na aba nº 1 (2ª aba);
  • linhas 42-44: decide-se excluir as abas existentes para recriar duas novas. Essa decisão foi tomada para contornar o seguinte problema: quando se limita a exibir o fragmento na aba 1 existente (sem, portanto, excluí-la), curiosamente seu título apresenta uma aparência (fonte, tamanho) diferente daquela do título da aba 0;
  • linhas 43-44: as duas abas são criadas, mas não selecionadas (último parâmetro em false);
  • linha 40: as operações das linhas 42-44 podem realizar operações [select] nas abas, o que chamará o gerenciador [onTabSelected]. Se nada for feito, haverá navegação para um fragmento. Isso é evitado definindo a variável booleana [navigationOnTabSelectionNeeded] como faux na sessão. Essa variável booleana é automaticamente redefinida como vrai pela classe [AbstractFragment] quando um fragmento se torna visível;
  • linha 46: registra-se o número do fragmento a ser exibido na sessão;
  • linhas 48-50: seleciona-se a aba nº 2 com navegação (linha 48). Isso acionará o procedimento [onTabSelected], que irá:
    • exibir o fragmento cujo número foi inserido na sessão;
    • armazenar na sessão o número da aba selecionada;

2.8.4.3. O fragmento [Vue1Fragment]

Apresentamos aqui a versão final do fragmento:


package client.android.fragments.behavior;

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

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

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

  // gerenciador de eventos
  @Click(R.id.buttonValider)
  protected void doValider() {
    // exibe-se o nome digitado
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

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

  // salvar estado do fragmento
  @Override
  public CoreState saveFragment() {
    // estado da visualização — nada a salvar
    return new CoreState();
  }

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

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

  @Override
  protected void initView(CoreState previousState) {
    // primeira visita?
    if (previousState == null) {
      // exibe o número da visita
      showNumVisit();
    }

  }

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

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

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

A classe está praticamente vazia.

  • linhas 35-39: chamadas pela classe pai quando o fragmento precisa salvar seu estado. O fragmento [Vue1Fragment] não tem estado a ser salvo. Simplesmente retorna-se uma instância da classe base [CoreState] (lembrete: não se deve retornar null);
  • linhas 41-44: devem retornar o número do fragmento. O fragmento [Vue1Fragment] tem, por definição, o número [FRAGMENTS_COUNT-1];
  • linhas 51-59: chamadas pela classe pai quando o fragmento é construído pela primeira vez (previousState==null) ou nas vezes seguintes (previousState!=null);
    • linhas 54-57: se for a primeira visita, incrementa-se o número de visitas e ele é exibido (linhas 85-92);
  • linhas 61-65: chamadas quando o fragmento for exibido associado a uma ação [SUBMIT]. Incrementa-se o número de visita e ele é exibido. Aqui, não é possível que o número de visita seja incrementado duas vezes no ciclo de vida. De fato, a primeira visita ao fragmento [Vue1Fragment] ocorre no início da aplicação, quando a ação é [NONE] por definição na sessão. Isso garante que o método [updateOnSubmit] não será chamado. Depois disso, nunca mais será a primeira visita e o método [initView] não fará nada;
  • linhas 68-71: chamadas em um ciclo de salvamento/restauração. Como o fragmento não possui estado, não há nada a ser restaurado aqui;
  • linhas 73-76: chamadas quando todas as atualizações anteriores tiverem sido realizadas. Aqui, não há mais nada a fazer;
  • linhas 78-81: chamadas quando todas as tarefas assíncronas iniciadas estiverem concluídas. Aqui, não há tarefas assíncronas;

2.8.4.4. O estado [PlaceHolderFragmentState]

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


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

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

  // construtores
  public PlaceHolderFragmentState() {

  }

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

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

2.8.4.5. O fragmento [PlaceHolderFragment]

O fragmento [PlaceHolderFragment] será o seguinte:


package client.android.fragments.behavior;

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

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

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

  // dados
  private String text;

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

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

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

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

  @Override
  protected void initView(CoreState previousState) {
  }

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

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

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

}
  • linhas 30-36: quando a classe pai solicita que o fragmento salve seu estado, salva-se o texto exibido pelo fragmento (linha 34);
  • linhas 38-41: retornam o número do fragmento. Esse número depende do número da seção que foi passado como argumento durante sua criação;
  • linhas 43-47: chamadas na primeira construção do fragmento (previousState==null) ou nas seguintes (previousState !=null);
    • linha 46: aqui, não se utiliza o estado anterior. O texto inicial [text] (linha 24), exibido na primeira visita, é recalculado a cada vez. Isso é questionável. Teria sido possível optar por incluir essa informação também no estado do fragmento;
  • linhas 49-51: chamadas na primeira construção da visualização associada ao fragmento (previousState==null) ou nas seguintes (previousState!=null). Não há nada a ser feito;
  • linhas 53-56: chamadas quando o fragmento for exibido associado a uma ação [SUBMIT]. Isso sempre ocorre, exceto no ciclo de salvamento/restauração, em que a ação é [RESTORE]. Portanto, incrementamos o número da visita e o exibimos;
  • linhas 68-74: chamadas em um ciclo de salvamento/restauração. Restaura-se o texto que havia sido salvo no estado do fragmento;
  • linhas 76-79: chamadas quando todas as atualizações anteriores foram realizadas. Aqui, não há mais nada a fazer;
  • linhas 82-83: chamadas quando todas as tarefas assíncronas iniciadas estiverem concluídas. Aqui, não há tarefas assíncronas;

2.8.4.6. Tests

Recomenda-se ao leitor que teste o aplicativo girando o dispositivo para verificar se o fragmento exibido não perde seu estado. Também será necessário verificar os logs.

2.9. Conclusion

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

  • a comunicação assíncrona com o servidor web / jSON é feita por meio da biblioteca RxJava;
  • o ciclo de vida de um fragmento (atualização, salvamento, restauração) é gerenciado por sua classe pai [AbstractFragment], que chama, em momentos específicos, determinados métodos de suas classes filhas. Assim, o fragmento filho não precisa se preocupar com as etapas do ciclo de vida, mas apenas implementar determinados métodos exigidos por sua classe pai;
  • o ciclo de vida da atividade (salvar/restaurar) é gerenciado por uma classe abstrata [AbstractActivity], que também exige que a atividade filha implemente determinados métodos;
  • a classe [AbstractActivity] é capaz de gerenciar um aplicativo com ou sem abas, com ou sem imagem de espera, com ou sem autenticação básica no servidor web / jSON. A presença ou ausência desses elementos é definida por configuração;

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