Skip to content

2. Skeleton of an Android client communicating with a web service / JSON

We now provide a skeleton for an Android application communicating with one or more web services / JSON. This is the [client-android-skel] project, which can be found in the [architecture] folder of the examples:

  

Studying this skeleton application will provide an opportunity to review certain points we encountered in the previous examples. This application will serve as a skeleton for all future applications. It was built after numerous iterations. It aims to factor as many elements as possible from the applications we will soon build into abstract classes to avoid having to write the same type of code over and over again, differing only in details. Its features are as follows:

  • asynchronous communication with the web server/JSON is handled using the RxJava library;
  • the lifecycle of a fragment (update, save, restore) is managed by its parent class [AbstractFragment], which calls certain methods of its child classes at specific times. The child class thus does not have to worry about the lifecycle stages but only needs to implement certain methods required by its parent class;
  • the activity’s lifecycle (save / restore) is managed by an abstract class [AbstractActivity], which also requires the child activity to implement certain methods;
  • The [AbstractActivity] class is capable of managing an application with or without tabs, with or without a loading image, and with or without basic authentication against the web server/JSON. The presence or absence of these elements is determined by configuration;

This skeleton was used for all subsequent examples. Due to their diversity, what worked for one example might not work for the next. Since the skeleton was used for a total of seven examples, numerous iterations took place. If we were to use it for an eighth example, it is possible that we would again find that the specific nature of this new example generates new errors. Nevertheless, the use of this skeleton will considerably simplify the writing of future examples. Indeed, managing a fragment’s lifecycle (update, save, restore) combined with the concept of fragment adjacency is particularly complex. Here, it is completely hidden within the [AbstractFragment] class.

2.1. Android Client Architecture

The proposed Android client is based on the following architecture:

  • the [DAO] layer implements an [IDao] interface. It is responsible for communicating with the web/JSON server;
  • there is only one activity that also implements the [IDao] interface. The views call upon it to access the server;
  • the views are implemented by fragments;

The Android project reflects this architecture:

  

We will present the various elements of this project one by one.

2.2. The Gradle configuration

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}
 
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
 
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
 
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
 
  // options de packaging nécessaires pour être capable de produire l'APK
  packagingOptions {
    exclude 'META-INF/ASL2.0'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/notice.txt'
    exclude 'META-INF/license.txt'
  }
}
 
def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  apt "org.androidannotations:rest-spring:$AAVersion"
  compile "org.androidannotations:rest-spring-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
  compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
  compile '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'
  }
}
  • All version numbers are subject to change. However, you can start with the current numbers if you configure Android Studio to ensure these versions of the Android tools (lines 15–16, 47–48) are present (see section 6.11);

2.3. The application manifest

 

<?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>
  • Line 3: Change the application package;
  • Lines 10, 15: We will set the value of the [app_name] item in the [res/values/strings.xml] file. For now, it is as follows:

<?xml version="1.0" encoding="utf-8"?>
<resources>
 
  <!-- application name -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

2.4. The organization of Java code

  
  • [architecture] groups the main elements of code organization;
  • [activity] contains the application's single activity;
  • [fragments] groups the application's fragments or views;
  • [dao] groups the elements for communication with the web server / JSON;

2.5. Activity Elements

 

Image

2.5.1. The view associated with the activity

The [activity_main.xml] view associated with the activity is as follows:


<?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>
 
  <!-- fragment container -->
  <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>
  • Line 29: A specific fragment container is used;

The activity also has a menu [res/menu/menu_main.xml] for its view:


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

For now, it is empty. The developer will fill it in as needed.

2.5.2. The fragment container [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 {
 
  // swipe control
  private boolean isSwipeEnabled;
  // controls scrolling
  private boolean isScrollingEnabled;
 
  // manufacturers
  public MyPager(Context context) {
    super(context);
  }
 
  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }
 
  // methods to be redefined to manage swiping
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // swipe allowed?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }
 
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // swipe allowed?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }
 
  // scrolling control
  @Override
  public void setCurrentItem(int position){
    super.setCurrentItem(position,isScrollingEnabled);
  }
 
  // setters
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }
 
  public void setScrollingEnabled(boolean scrollingEnabled) {
    isScrollingEnabled = scrollingEnabled;
  }
}

This class extends the standard Android [ViewPager] class solely to handle swiping (line 11) and scrolling (line 13) between views.

  • lines 26–43: methods that disable swiping if it has been turned off;
  • lines 46–49: redefinition of the [setCurrentItem] method, which is used to change the displayed view. If scrolling has been disabled, the view will change without scrolling. Note that the developer can override this behavior by using the [setCurrentItem(int position, boolean smoothScrolling)] method, which allows them to specify the desired scrolling behavior;

2.5.3. The [CoreState] class

  

The [CoreState] class is the parent class for the states of the various fragments:


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)
// todo: add subclasses of [CoreState] here
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • line 16: Each fragment has a boolean [hasBeenVisited] in its state that indicates whether it has already been visited or not. This is necessary because sometimes, when a fragment is first displayed, there are specific actions that need to be taken;
  • line 18: the [client-android-skel] project automatically saves and restores fragment menus if they have one. In the MenuItemState[] menuOptionsState array, we store the visible or hidden state of all menu options;
  • Lines 10–13: As done in [Example-22], the state of the activity and its fragments will be saved in the session, which will itself be saved as a JSON string. We will see that the session stores an array of elements of type [CoreState]. If we do nothing, then the JSON string of type [CoreState] will be saved. However, we want to save the states of the fragments, which are derived from [CoreState]. To ensure that the JSON string of the derived type is generated rather than that of the parent type, the derived types must be declared as shown in lines 10–13. The [CoreState] class is one of the architecture classes that the developer must modify for each new application (lines 10–13);

2.5.4. The [IMainActivity] interface

  

The [IMainActivity] interface defines what fragments can request from the activity in the following architecture:

Image


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // application constants (to be modified) -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 0;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = false;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 0;
 
  // todo add your constants and other methods here
}
  • line 6: the [IMainActivity] interface extends the [IDao] interface of the [DAO] layer;
  • Line 9: This is the activity that provides access to the session in the form of an instance of the [ISession] interface;
  • line 12: this is the activity used to switch views. The second parameter is the action that triggers this view change, one of the values SUBMIT, NAVIGATION, or RESTORE;
  • lines 15–17: this is the activity that manages the loading screen;
  • line 22: for debugging the application;
  • line 25: to avoid waiting too long if the server stops responding;
  • line 28: during debugging, set this to a few seconds to allow time to cancel the operation with the server and see what happens;
  • line 31: set to true if the JSON service requires basic authentication;
  • line 34: fragment adjacency;
  • line 37: set to true if the application has tabs;
  • line 39: set to true if the application communicates with a web/JSON server and you want to display a loading image during exchanges;
  • line 43: the number of fragments managed by the application;

The [IMainActivity] interface is the second element of the architecture that the developer must implement (line 45).

2.5.5. The [IDao] interface

The [IMainActivity] interface extends the following [IDao] interface:

  

package client.android.dao.service;
 
import rx.Observable;
 
public interface IDao {
  // Web service url
  void setUrlServiceWebJson(String url);
 
  // user
  void setUser(String user, String mdp);
 
  // customer timeout
  void setTimeout(int timeout);
 
  // basic authentication
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // todo: declare your interface here
}
  • line 24: the developer will complete the interface here;

2.5.6. The Session

  

The [Session] class encapsulates elements shared by the activity and fragments. It implements the following [ISession] interface:


package client.android.architecture.core;
 
import client.android.architecture.custom.CoreState;
 
public interface ISession {
 
  // number of last view displayed
  int getPreviousView();
 
  void setPreviousView(int numView);
 
  // last state of a view
  CoreState getCoreState(int numView);
 
  void setCoreState(int numView, CoreState coreState);
 
  // action in progress
  enum Action {
    SUBMIT, NAVIGATION, RESTORE, NONE
  }
 
  Action getAction();
 
  void setAction(Action action);
 
  // status of all views -
  // not used by code but required for serialization / deserialization jSON
  CoreState[] getCoreStates();
 
  void setCoreStates(CoreState[] coreStates);
 
  // last selected tab number
  int getPreviousTab();
 
  void setPreviousTab(int position);
 
  // tab selection navigation
  boolean isNavigationOnTabSelectionNeeded();
 
  void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}

We introduce the [ISession] interface to require the presence of certain methods in the session:

  • lines 7–10: the number of the last view (fragment) displayed;
  • lines 12–15: the state of a particular view;
  • lines 17–24: we introduce the concept of an ongoing action. There are four (line 17):
    • RESTORE: a save/restore operation is in progress. There is no view change;
    • NAVIGATION: navigation is in progress. Here, we define navigation as a view change where the new view can be restored from its last state saved during the session;
    • SUBMIT: we assign the type [SUBMIT] to a pending action when there is a view change and the new view depends on the overall state of the activity, not just its own state. Sometimes, it is difficult to distinguish between NAVIGATION and SUBMIT. In such cases, we will use the more general case of SUBMIT;
    • NONE: the action’s value when it has not yet received its first value;
  • lines 26–30: the states of the activity and fragments will be stored in a CoreState[] array. To ensure this is handled correctly during JSON serialization/deserialization, it must have a getter and a setter;
  • lines 32-35: number of the last selected tab. Used during the save/restore cycle to reselect the tab that was selected before the device was rotated;
  • lines 37–40: manages a boolean that indicates whether selecting a tab should be accompanied by a fragment change;

The [ISession] interface is implemented by the following abstract class [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 {
  // previous view no
  private int preViousView;
 
  // view status
  private CoreState[] coreStates = new CoreState[0];
 
  // action in progress
  private Action action = Action.NONE;
 
  // previously selected tab
  private int previousTab;
 
  // tab selection navigation
  @JsonIgnore
  private boolean navigationOnTabSelectionNeeded = true;
 
  // manufacturer
  public AbstractSession() {
    // initialize the fragment status table
    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;
  }
}
  • line 9: the ID of the view that was displayed before the currently displayed one. This information is useful when a view can be reached from multiple locations. This is typically the case in tab-based navigation. The displayed view can then determine which view was displayed previously;
  • line 12: the array of states for all fragments displayed by the activity;
  • line 18: the ID of the previously selected tab. Plays a role similar to that of the previous view ID in line 9. This information is useful when the device is rotated and you need to return to the tab that was selected before the rotation;
  • line 22: a boolean indicating whether selecting a tab should result in a change to the displayed fragment. Note that the [client-android-skel] project manages tabs and fragments separately so that it can be used in cases where the number of tabs is fewer than the number of fragments. There are two types of selection:
    • a selection made by the user when they click on a tab. In this case, the displayed fragment generally needs to change;
    • a software-driven selection via the [Tablayout.Tab.select()] method. In this case, changing the displayed fragment is not always desirable. Here are two examples:
      • when the device is rotated, the activity is recreated, and so are the tabs. However, when the first tab is created, it automatically undergoes a software [select] operation. It is therefore not desirable to change the displayed fragment because we are in a phase of recreating the activity where the fragment ultimately displayed will not necessarily be the one associated with the first tab;
      • since tab management is separate from fragment management, you may want to update the tabs (delete, add) without interfering with their associated fragments. However, some of these operations can again trigger an implicit [select] software operation on one of the tabs. This selection does not necessarily result in navigation to the associated fragment;
  • line 21: the [navigationOnTabSelectionNeeded] field is not intended to be saved during save operations for the activity and its fragments. The [@JsonIgnore] annotation causes the field to be ignored during JSON serialization/deserialization;
  • lines 25–31: The constructor initializes the array of states for the [FRAGMENTS_COUNT] fragments of the application. The elements of this array are initialized with the field [hasBeenVisited=false]. This information is used to determine whether or not this is the first visit to the fragment;

The [Session] class is as follows:


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // data to be shared between fragments themselves and between fragments and activities
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
  // don't forget the getters and setters required for serialization / deserialization jSON
}
  • line 5: the [Session] class extends the [AbstractSession] class we just saw. The developer will place the elements to be shared between fragments themselves and between fragments and the activity here. Note that the [Session] class is no longer annotated with the [@EBean] annotation. It has become a normal class;

2.5.7. The abstract class [AbstractActivity]

  

2.5.7.1. Skeleton

The [AbstractActivity] class is a class with over 300 lines. We will examine it step by step. Its skeleton is as follows:


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 {
  // layer [DAO]
  private IDao dao;
  // the session
  protected Session session;
 
  // the fragment container
  protected MyPager mViewPager;
  // the toolbar
  private Toolbar toolbar;
  // the waiting image
  private ProgressBar loadingPanel;
  // tab bar
  protected TabLayout tabLayout;
 
  // the fragment or section manager
  private FragmentPagerAdapter mSectionsPagerAdapter;
  // class name
  protected String className;
  // mapper jSON
  private ObjectMapper jsonMapper;
 
  // manufacturer
  public AbstractActivity() {
    // class name
    className = getClass().getSimpleName();
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "constructeur");
    }
    // jsonMapper
    jsonMapper = new ObjectMapper();
  }
 
  // implémentation IMainActivity --------------------------------------------------------------------
  ...
 
  // life cycle - backup / restore ------------------------------------
  ...
 
  // hold image management ---------------------------------
  ...
 
  // interface IDao -----------------------------------------------------
  ...
 
  // the fragment manager --------------------------------
  ...
 
  // girls' classes
  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();
 
}

The [AbstractActivity] class:

  • implements the [IMainActivity] interface (lines 21, 55);
  • handles the saving and restoration of the activity and its fragments when the device rotates (line 58);
  • handles the loading screen during communication with the web server / JSON (line 61);
  • implements the IDao interface of the [DAO] layer (line 64);
  • implements the fragment manager (line 67);
  • requires its child classes to have six methods (lines 71–81);

2.5.7.2. Implementing the [IMainActivity] interface

The implementation of the [IMainActivity] interface (see section 2.5.4) is as follows:


  // implémentation 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));
    }
    // display new fragment
    mViewPager.setCurrentItem(position);
    // we note the action in progress when the view changes
    session.setAction(action);
}

2.5.7.3. Saving the state of the activity and its fragments

The state of the activity and its fragments is entirely contained within the session. Therefore, we need to save the session. Here, we reuse what was done in the [Example-22] project (see section 1.23):


  // backup / restore management ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // parent
    super.onSaveInstanceState(outState);
    // save session as jSON string
    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. Restoring the state of the activity and its fragments

This involves restoring the session. We proceed as shown in [Example-22]:


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    // something to restore?
    if (savedInstanceState != null) {
      // session recovery
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // log
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    } else {
      // session
      session = new Session();
    }
...
  • lines 10–26: if the [Bundle savedInstanceState] parameter on line 2 is not null, then the session is restored (lines 12–17);
  • lines 26–29: if the [Bundle savedInstanceState] parameter on line 2 is null, this corresponds to the first time the activity is launched. An empty session is then created;

2.5.7.5. Initialization of the [DAO] layer


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    ...
    // layer [DAO]
    dao = getDao();
    if (dao != null) {
      // layer configuration [DAO]
      setDebugMode(IS_DEBUG_ENABLED);
      setTimeout(TIMEOUT);
      setDelay(DELAY);
      setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
    }
...
  // girls' classes
  protected abstract IDao getDao();
....
}
  • line 11: a reference to the [DAO] layer is requested from the child activity (line 21);
  • lines 14–17: if the [DAO] layer exists, it is configured using the information contained in the [IMainActivity] interface;

2.5.7.6. Initialization of the view associated with the activity

The view associated with the activity was presented in Section 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>
 
  <!-- fragment container -->
  <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>

This view is initialized with the following code:


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // associated view
    setContentView(R.layout.activity_main);
    // view components ---------------------
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // waiting image?
    if (IS_WAITING_ICON_NEEDED) {
      // we add the waiting image
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding loadingPanel");
      }
      // creation ProgressBar
      loadingPanel = new ProgressBar(this);
      loadingPanel.setVisibility(View.INVISIBLE);
      // added ProgressBar to toolbar
      toolbar.addView(loadingPanel);
    }
...
  • line 11: the XML view [activity_main] is associated with the activity;
  • lines 14-15: the toolbar is integrated and supported;
  • lines 17-27: optional addition of a loading icon: if the boolean [IS_WAITING_ICON_NEEDED] is true in the [IMainActivity] interface;
  • line 23: creation of the [ProgressBar]-type loading image referenced by the [loadingPanel] field;
  • line 24: initially, this image is hidden;
  • line 26: it is added to the toolbar;

2.5.7.7. Tab Management

The [IMainActivity] interface may request a tab bar. This is added and managed as follows:


// tab bar
  protected TabLayout tabLayout;
...
 
    // tab bar?
    if (ARE_TABS_NEEDED) {
      // add the tab bar
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding tablayout");
      }
      // no selection navigation until a fragment is displayed
      session.setNavigationOnTabSelectionNeeded(false);
      // tab bar creation
      tabLayout = new CustomTabLayout(this);
      tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
      // tab bar added to application bar
      AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
      appBarLayout.addView(tabLayout);
      // tab bar event manager
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
          // a tab has been selected
          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()) {
            // miter position
            int position = tab.getPosition();
            // memory
            session.setPreviousTab(position);
            // associated fragment display?
            navigateOnTabSelected(position);
          }
        }
 
        @Override
        public void onTabUnselected(TabLayout.Tab tab) {
 
        }
 
        @Override
        public void onTabReselected(TabLayout.Tab tab) {
 
        }
      });
    }
 
...
  // girls' classes
  protected abstract void navigateOnTabSelected(int position);
...
  • lines 12–48: adding and managing a tab bar;
  • line 6: the tab bar is added if the constant [ARE_TABS_NEEDED] is set to true in the [IMainActivity] interface;
  • line 12: when creating the tab bar, implicit [Tablayout.Tab.select] operations may occur (these are not triggered by the user). We set the boolean [session.navigationOnTabSelectionNeeded] to false to prevent any navigation during these false selections. It will be up to the developer to select the fragment to display using the [navigateToView] method. The boolean [session.navigationOnTabSelectionNeeded] will be set back to true when this fragment is displayed (see AbstractFragment class);
  • line 14: creation of a tab bar referenced by the [tabLayout] field. We use a custom tab bar [CustomTabLayout], which we will discuss later;
  • line 15: we set the colors of the tab titles. These are found in the following [res/color/tab_txt.xml] file:

<?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>
    • line (c): the color of the tab title when the tab is selected;
    • line (d): the color of the tab title when it is not selected;

This file is, of course, editable. You can find the hexadecimal color codes here, for example.

  • lines 17-18: adding this tab bar to the application bar in the [activity_main] XML view;
  • lines 20–47: event handler for the tab bar;
  • lines 22–36: only the [onTabSelected] event is handled. It corresponds to a click on the [Tab tab] passed as a parameter to the method or to a software operation [TabLayout.Tab.select];
  • line 30: position of the selected tab;
  • line 32: this position is stored in the session;
  • line 34: the fragment associated with this tab must now be displayed. Only the child class (line 52) can make this association. Note that we do not associate the tab bar with the fragment container [mViewPager] as was done in some of the examples studied. Here, we completely separate the management of the tab bar from that of the fragments. This is why, when a tab is clicked, we must specify which view we want to see displayed;
  • line 28: we distinguish between tab selection with or without navigation. Generally, when the user clicks a tab, navigation is expected, whereas during a programmatic selection, it is not. The developer distinguishes between these two cases using the [session.navigationOnTabSelectionNeeded] element. When navigation is not performed, the number of the last selected tab is not saved in the session. It is up to the developer to do so;

2.5.7.8. The [CustomTabLayout] tab manager

  

We use a custom tab manager to display tab titles in different fonts. The [CustomTabLayout] class is as follows:


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);
      }
    }
  }
 
}
  • The font for the tab titles is customized on lines 30 and 44;

The [fonts] folder is as follows:

  

Sources:

2.5.7.9. Latest initializations


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // fragment manager instantiation
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i delivered by the fragment manager
    mViewPager = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
    // fragment adjacency
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    // display 1st view
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    // we hand over to the daughter activity
    onCreateActivity();
  }
...
  // girls' classes
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • lines 10–19: This code is commonly found in the examples we’ve studied;
  • lines 21–23: Display of the very first view. There are undoubtedly several ways to distinguish this case. Here, we have used the fact that for the very first view, the value of the action that triggers the view change is NONE;
  • line 22: we make no assumptions about the first fragment to display. In our examples, this has often been fragment #0, but not always (see Example-22). We will therefore ask the child activity (line 30) to tell us which view this is;
  • line 25: we have factored out everything we could here. Now, the child class has its own initializations to perform (line 29);

2.5.7.10. Handling the Loading Image

In the [AbstractActivity] class, the placeholder image is managed by the following two methods:


  // hold image management ---------------------------------
  public void cancelWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.INVISIBLE);
    }
  }
 
  public void beginWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.VISIBLE);
    }
}

2.5.7.11. Implementation of the [IDao] interface

In the [AbstractActivity] class, the [IDao] interface (see section 2.5.5) is implemented as follows:


public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // layer [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);
}
  • Line 3: Recall that the value of this field was provided by the child activity in the [onCreate] method;

2.5.7.12. Implementation of the fragment manager

In the [AbstractActivity] class, the fragment manager is implemented as follows:


...
  // the fragment manager --------------------------------
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    private AbstractFragment[] fragments;
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
      // daughter class fragments
      fragments = getFragments();
    }
 
    // must render fragment no. position
    @Override
    public AbstractFragment getItem(int position) {
      // we return the fragment
      return fragments[position];
    }
 
    // makes the number of fragments to manage
    @Override
    public int getCount() {
      return fragments.length;
    }
 
    // makes the title of fragment no. position
    @Override
    public CharSequence getPageTitle(int position) {
      return getFragmentTitle(position);
    }
  }
 
  // girls' classes
  protected abstract AbstractFragment[] getFragments();
 
  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • line 5: the array of fragments associated with the activity. All fragments will be derived from the [AbstractFragment] class;
  • lines 8–12: this is the constructor that initializes the array of fragments. It requests these from the activity’s child class (line 35);
  • lines 28–31: fragment titles can be used in an application where there are as many tabs as there are fragments. In this case, the tab can be given the fragment’s title. Here, these titles are requested from the child class (line 37);

2.5.7.13. The [onResume] method

The [onResume] method is executed shortly before the view associated with the activity becomes visible. It is used here to select a tab after a save/restore operation:


  @Override
  public void onResume() {
    // parent
    super.onResume();
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onResume");
    }
    // if restore, then restore last selected tab
    if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
      tabLayout.getTabAt(session.getPreviousTab()).select();
    }
}
  • Line 10: Selection of the tab that was selected before the save/restore process. It is important to note here that in the [onCreate] method—which, in the activity lifecycle, is executed before the [onResume] method—navigation upon selecting a tab has been disabled. Therefore, here, a tab is selected but there is no fragment change;

2.5.7.14. Summary

The abstract class [AbstractActivity] will be the parent class of the application’s single activity.

The child activity must implement the following six methods:


  // girls' classes
  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();

The child activity also has access to the following protected members of its parent class:


  // the session
  protected ISession session;
  // the fragment container
  protected MyPager mViewPager;
  // tab bar
  protected CustomTabLayout tabLayout;
  // class name
protected String className;

2.5.8. The [MainActivity] activity

  

The [MainActivity] class can have a different name. Its only requirement is to implement the [IMainActivity] interface. The default class provided is as follows:


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 {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
    // todo: we continue the initializations started by the parent class
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    // todo: define fragments here
    return new AbstractFragment[0];
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    // todo: define fragment titles here
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // todo: tabbed browsing - define the view to be displayed
  }
 
  @Override
  protected int getFirstView() {
    // todo: tabbed browsing - define the first view to be displayed
    return 0;
  }
}
  • line 14: for the AA [@Bean] annotation on line 19 to be valid, the activity must have the AA [@EActivity] annotation;
  • line 15: the activity is associated with the XML menu [menu_main]. Currently, this menu is empty. The developer will need to fill it in if required;
  • line 16: the class extends the [AbstractActivity] class;
  • Lines 19–20: a reference to the [DAO] layer. This will be instantiated by the AA library before this field is initialized. This means that the AA [Dao] bean must exist. This is always the case with the skeleton application we provide. Even in an application without a [DAO] layer, you can leave the [dao] package in place. This does not cause any complications;
  • line 22: the session as an instance of type [Session]. The session exists in the parent class [AbstractActivity] but as an instance of the interface [ISession] (line 32);
  • lines 24–63: the six methods required by the parent class [AbstractActivity];
  • Lines 36–39: The [getDao] method returns a reference to the [DAO] layer. Here, this reference is never null. However, in the parent class [AbstractActivity], we have provided for the case where the child class returns a null reference to indicate that there is no [DAO] layer. If you wish to use this option (not very useful in my opinion), this is where you must set the pointer to null;

2.6. The [DAO] layer

Image

  

2.6.1. The IDao interface

It was introduced in section 2.5.5:


package client.android.dao.service;
 
import rx.Observable;
 
public interface IDao {
  // Web service url
  void setUrlServiceWebJson(String url);
 
  // user
  void setUser(String user, String mdp);
 
  // customer timeout
  void setTimeout(int timeout);
 
  // basic authentication
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // todo: declare your interface here
}

The developer will add the methods for their [DAO] layer starting on line 24.

2.6.2. The [WebClient] interface

  

The [WebClient] interface is as follows:


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);
 
  // todo : declare here the URL to be reached
}

The developer will add the methods that communicate with the URLs exposed by the JSON server starting on line 17.

2.6.3. The authentication interceptor [MyAuthInterceptor]

  

The [MyAuthInterceptor] class is as follows:


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 {
 
  // user
  private String user;
  // password
  private String mdp;
 
  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // headers HTTP of the HTTP request intercepted
    HttpHeaders headers = request.getHeaders();
    // the HTTP basic authentication header
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    // added to HTTP headers
    headers.setAuthorization(auth);
    // continue the query life cycle HTTP
    return execution.execute(request, body);
  }
 
  // authentication elements
  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}

This class generates the following HTTP authentication header:

Authorization: Basic code

where [code] is the Base64-encoded string 'user:mp'. This class is only used if the JSON server expects this form of authentication. Other forms exist.

Note: The use of this class is illustrated in section 3.6.3.1.

2.6.4. The [AbstractDao] class

  

The [AbstractDao] class is as follows:


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 {
 
  // mapper jSON
  private ObjectMapper mapper = new ObjectMapper();
  // debug mode
  protected boolean isDebugEnabled;
  // class name
  protected String className;
  // timeout before request execution
  private int delay;
 
  // manufacturer
  public AbstractDao() {
    // class name
    className = getClass().getName();
    Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
  }
 
  // méthodes protégées ----------------------------------------------------------
  // generic interface
  protected interface IRequest<T> {
    T getResponse();
  }
 
  // generic request to a web service / jSON
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("delay=%s", delay));
    }
    // service execution - a single response is expected
    return Observable.create(new Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // service execution
        try {
          // waiting?
          if (delay > 0) {
            Thread.sleep(delay);
          }
          // execute the synchronous request
          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()));
          }
          // the response is sent to the observer
          subscriber.onNext(response);
          // we signal the end of the observable
          subscriber.onCompleted();
        } catch (InterruptedException | JsonProcessingException | RuntimeException e) {
          // log
          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"));
            }
          }
          // an exception is issued
          subscriber.onError(new DaoException(e, 100));
        }
      }
    });
  }
 
  // debug mode
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
  }
 
  public void setDelay(int delay) {
    this.delay = delay;
  }
}
  • lines 35–81: the [getResponse] method uses the RxAndroid library to return a [Observable<T>] type. Unlike some examples seen previously, it does not return a [Response<T>] type—which is a proprietary type—but rather any type T;
  • line 35: the [getResponse] method takes as a parameter an instance of type [IRequest<T>] from lines 30–32, whose [IRequest.getResponse()] method obtains type T via a synchronous HTTP operation;
  • lines 48–50: artificially, we wait [delay] milliseconds. In production, we will set [delay=0]. During debugging, we will set [delay=a few seconds] to give the user a chance to cancel the asynchronous operation and thus see how the code behaves in that case;
  • line 52: the expected response is requested with a synchronous request;
  • line 64: once the response is received, it is passed to the observer;
  • line 66: we indicate that there will be no further emissions. This is the specific case of an asynchronous action that returns only one element;
  • lines 67–78: in the event of an exception, the exception is propagated to the observer (line 77);

2.6.5. The [Dao] class

  

The [Dao] class is as follows:


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 {
 
  // web service customer
  @RestService
  protected WebClient webClient;
  // safety
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // on RestTemplate
  private RestTemplate restTemplate;
  // factory du RestTemplate
  private SimpleClientHttpRequestFactory factory;
 
  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // we build the restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // set the jSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // set the restTemplate of the web client
    webClient.setRestTemplate(restTemplate);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    // set the URL of the web service
    webClient.setRootUrl(url);
  }
 
  @Override
  public void setUser(String user, String mdp) {
    // the user is registered in the interceptor
    authInterceptor.setUser(user, mdp);
  }
 
  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // factory configuration
    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));
    }
    // authentication interceptor?
    if (isBasicAuthentificationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }
 
  // méthodes privées -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // todo: implementation IDao
}
  • lines 21-22: injection of the AA [WebClient] bean, which will handle communication with the web server / JSON;
  • lines 24-25: injection of the authentication interceptor;
  • lines 31-42: method executed after injection of the fields from lines 21-25;
  • line 37: the [RestTemplate] object, which handles client/server communication, is created from a factory. This isn’t strictly necessary, but the factory allows us to configure communication timeouts. That’s why we don’t use the parameterless constructor [RestTemplate()];
  • line 39: we add a JSON converter to the [RestTemplate]’s converters. This will be the only converter. Thus, when a method of the [WebClient] receives a JSON string from the server, it will be automatically deserialized into the object that the method is supposed to return;
  • line 41: the [RestTemplate] object configured in this way is passed to the web client, which will handle client/server communication using it;
  • Lines 44–48: We set the root URL of the web/JSON server. All URLs declared in the [WebClient] class are relative to this root URL;
  • lines 50–54: this method allows you to specify the connection owner when the connection is controlled by basic authentication (see section 2.6.3);
  • lines 56–64: set the timeouts for client/server exchanges. This is done via the [RestTemplate] object’s factory, which governs the exchanges;
  • lines 66–78: this method specifies that the server is protected by basic authentication;
  • lines 72–77: if basic authentication is required, the authentication interceptor injected on line 25 is added to the [RestTemplate] object’s interceptors. This interceptor will automatically add the basic authentication HTTP header expected by the server to all web client requests;
  • The developer will implement the [IDao] interface starting on line 87;

2.7. Fragments

  

2.7.1. The [MenuItemState] class

The [MenuItemState] class encapsulates the state of a menu option:


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

2.7.2. The [Utils] class

The [Utils] class contains static utility methods:


package client.android.architecture;
 
import java.util.ArrayList;
import java.util.List;
 
public class Utils {
 
  // list of exception messages - version 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // create a list of error msgs from the exception stack
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }
 
  // exception message list - version 2
  static public String getMessageForAlert(Throwable th) {
    // build the text to be displayed
    StringBuilder texte = new StringBuilder();
    List<String> messages = getMessagesFromException(th);
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // result
    return texte.toString();
  }
 
  // list of exception messages - version 3
  static public String getMessageForAlert(List<String> messages) {
    // build the text to be displayed
    StringBuilder texte = new StringBuilder();
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // result
    return texte.toString();
  }
}

2.7.3. The parent class [AbstractFragment]

The [AbstractFragment] class contains the elements common to all fragments in the application. As in the [AbstractActivity] class, its code is complex. We will also analyze it step by step.

2.7.3.1. The skeleton


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 {
 
  // données privées ------------------------------------------------------------
  // subscriptions to observables
  private List<Subscription> abonnements = new ArrayList<>();
  // fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates = new MenuItemState[0];
  // fragment life cycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment status
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
  // asynchronous tasks
  private boolean runningTasksHaveBeenCanceled;
 
  // data accessible to daughter classes ---------------------------------------
  // debug mode
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // class name
  protected String className;
  // asynchronous tasks
  protected int numberOfRunningTasks;
  // activity
  protected IMainActivity mainActivity;
  protected Activity activity;
  // session
  protected Session session;
 
 
  // update Fragment ----------------------------------------------------------------------------------
 ...
 
  // menu management ------------------------------------------
  ...
 
  // wait management -------------------------------------------------------------
...
 
  // asynchronous operation management --------------------------------------------------------------------
...
 
  // gestion exception -------------------------------------------------------------------
....
 
  // fragment lifecycle management --------------------------------------------------------
...
 
  // classes filles -----------------------------------------------------
  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);
 
}
  • lines 28–45: the class’s private data;
  • lines 47–58: protected data accessible by child classes;
  • lines 61–62: code that updates the fragment to be displayed;
  • lines 64-65: utility code to handle the menu, if present;
  • lines 67-68: utility code to handle waiting during an asynchronous operation;
  • lines 70–71: code to facilitate communication between the fragment and the [DAO] layer;
  • lines 73-74: utility code to handle any exceptions in a standard way;
  • lines 76-77: code managing the fragment's lifecycle;
  • lines 80–94: the parent class imposes 8 methods on its child classes;

2.7.3.2. The constructor

The class constructor is as follows:


  // class name
  protected String className;
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
...
  // constructeur ----------------------
  public AbstractFragment() {
    // init
    className = getClass().getSimpleName();
    fragmentHasToBeInitialized = true;
    // log
    if (isDebugEnabled) {
      Log.d(className, "constructeur");
    }
}
  • Line 9: The name of the child class being instantiated here is noted. This name is used in all logs of the parent class;
  • line 10: we note that the fragment is being constructed. This information will be used when the child fragment is asked to update itself;

2.7.3.3. Menu Management

In our architecture, every fragment must have a menu, even if it is empty. The logs have indeed shown that when the [onCreateOptionsMenu] method—which runs when the fragment has a menu—executes, the fragment has already been associated with its activity, view, and menu and is about to become visible. This is therefore the moment when the visual interface and the menu can be updated. It is within this [onCreateOptionsMenu] method that we instruct the child fragment to update itself.

Menu management includes utility methods that allow the child fragment to display or hide menu items:


  // fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
...
  // menu management ------------------------------------------
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // scroll through all menu items
    for (int i = 0; i < menu.size(); i++) {
      // item n° i
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // if item n° i is a sub-menu, then start again
      if (menuItem.hasSubMenu()) {
        // recursivity
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
  }
 
  private void getMenuOptionsStates(Menu menu) {
    // result
    if (isDebugEnabled) {
      Log.d(className, "getMenuOptionsStates(Menu)");
    }
    // we retrieve the identifiers of the menu options
    List<Integer> menuOptionsIds = new ArrayList<>();
    getMenuOptions(menu, menuOptionsIds);
    // transfer menu options to a table
    menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // identify option
      int id = menuOptionsIds.get(i);
      // status option
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // result
    if (isDebugEnabled) {
      Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
    }
  }
 
  // menu option status
  private MenuItemState[] getMenuOptionsStates() {
    MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // status
      MenuItemState state = this.menuOptionsStates[i];
      // menu id
      int id = state.getMenuItemId();
      // initialization status
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // result
    return menuOptionsStates;
  }
 
  // display menu options -----------------------------------
  protected void setAllMenuOptionsStates(boolean isVisible) {
    // update all menu options
    for (MenuItemState menuItemState : menuOptionsStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
    }
  }
 
  protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
    // update certain menu options
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}
  • lines 6–18: this method retrieves the numerical identifiers of all menu options;
  • line 6: the [getMenuOptions] method takes two parameters:
    • [Menu menu]: the fragment's menu;
    • [List<Integer> menuOptionsIds]: the list of Android IDs for the menu options. Initially, this list is empty. It is then populated by a recursive traversal (line 15) of the menu tree;
  • lines 20–40: based on the menu, constructs the array of states (ID, visibility) for the menu options. This array is stored in line 3. The [MenuItemState] class was described in section 2.7.1;
  • lines 43–55: a variant of the previous method. It does the same thing, but instead of recalculating the identifiers for all menu options—which has already been done—it uses the identifiers from the state array in line 3;
  • lines 58–63: the [setAllMenuOptionsStates] method allows you to hide or show all of the fragment’s menu options;
  • lines 65–69: the [setMenuOptionsStates] method allows you to selectively show or hide certain menu options;
  • The methods [getMenuOptions, getMenuOptionsStates] are declared private because they are used only within [AbstractFragment]. The methods [setAllMenuOptionsStates] (line 58) and [setMenuOptionsStates] (line 65) are declared protected so that they are available to child classes;

2.7.3.4. Handling the wait for the completion of an asynchronous task


   // subscriptions to observables
  private List<Subscription> abonnements = new ArrayList<>();
// asynchronous tasks
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...
 
  // management of waiting for the end of an asynchronous operation -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // the number of tasks to be executed is noted
    this.numberOfRunningTasks = numberOfRunningTasks;
    // we put the image on hold
    mainActivity.beginWaiting();
    // empty the subscription list
    abonnements.clear();
    // no cancellations yet
    runningTasksHaveBeenCanceled = false;
  }
 
  protected void cancelWaitingTasks() {
    // we hide the waiting image
    mainActivity.cancelWaiting();
  }
 
  • lines 9–18: To start one or more asynchronous operations, the child fragment will call the parent method [beginRunningTasks]. The parameter of this method is the number of asynchronous tasks that the child fragment will launch;
  • line 11: we store the method’s parameter;
  • line 13: the loading screen is made visible;
  • line 15: the list of subscriptions to asynchronous operations is cleared. These have not yet been created by the child fragment;
  • line 17: a boolean is maintained to indicate that the asynchronous tasks requested by the child fragment have been canceled. Initially, the boolean has the value false;
  • lines 20–25: the child fragment calls the parent method [cancelWaitingTasks] to indicate that it wants to cancel the tasks it has launched;
  • line 22: the waiting image is hidden;

2.7.3.5. Exception Handling


  // gestion exception -------------------------------------------------------------------
 
  // exception alert display
  protected void showAlert(Throwable th) {
    // display messages from the Throwable th exception stack
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
  }
 
  // message list display
  protected void showAlert(List<String> messages) {
    // the message list is displayed
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
  • lines 4-7: the [showAlert(Throwable)] method allows a child fragment to display the messages from the exception stack of the Throwable passed as a parameter in a window;
  • lines 10–13: the [showAlert(List<String>)] method allows a child fragment to display the list of messages passed as a parameter in a window;
  • The [Utils] class used in lines 6 and 12 was described in section 2.7.2;

2.7.3.6. Handling asynchronous operations


...
  // subscriptions to observables
  private List<Subscription> abonnements = new ArrayList<>();
  // asynchronous tasks
  private boolean runningTasksHaveBeenCanceled;
  protected int numberOfRunningTasks;
...
  // asynchronous task execution with RxAndroid
  protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
    // process: the observable to be executed/observed
    // consumeResult: the method that uses the response obtained
    // 
    // new subscriptions are only created if they have not been cancelled
    if (!runningTasksHaveBeenCanceled) {
      // execution on I/O thread and observation on Ui thread
      process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
      // the observable is executed
      try {
        abonnements.add(process.subscribe(
          // consumption result
          consumeResult,
          // consumption exception
          new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
              consumeThrowable(th);
            }
          },
          // end of task
          new Action0() {
 
            @Override
            public void call() {
              endOfTask();
            }
          }));
      } catch (Throwable th) {
        consumeThrowable(th);
      }
    }
  }
 
  private void endOfTask() {
...
  }
 
  // an asynchronous operation has thrown an exception
  // or an exception has occurred during the execution of an asynchronous operation
  private void consumeThrowable(Throwable th) {
...
  }
 
  • lines 9–41: execute an asynchronous task;
  • line 9: the [executeInBackground] method expects two parameters:
    • [Observable<T> process]: the asynchronous process to be executed;
    • [Action1<T> consumeResult]: the method of the child fragment to be called to pass it the elements emitted by the process. In our previous examples, the processes have always emitted only one element. The type T of [Action1<T>] is the type T of the result returned by the observed process;
  • line 14: the asynchronous task is only launched if it has not already been canceled by the user or by the program (due to an exception);
  • line 16: the process is configured to run on an I/O thread and observed on the UI thread;
  • line 16: the [process.subscribe] statement launches the process in the I/O thread. Within this thread, operations execute synchronously because we are using a synchronous HTTP library;
  • line 19: the [process.subscribe] method has three parameters:
    • line 21: [consumeResult]: the child fragment’s method that will consume the elements emitted by the process;
    • lines 22–28: the method executed when an exception occurs during processing of the asynchronous task. Handling is delegated to the [consumeThrowable] method on line 49;
    • lines 29–36: the method executed when the task emits the end-of-emission notification. The handling is delegated to the [endOfTask] method on line 43;
  • line 19: the asynchronous task that has just been launched is recorded in the [subscriptions] field, which tracks all launched asynchronous tasks. This will allow them to be canceled if necessary;
  • lines 37–39: method executed when an exception occurs during processing of the asynchronous task. The handling is delegated to the [consumeThrowable] method on line 49;

The [endOfTask] method is as follows:


  // asynchronous tasks
  protected int numberOfRunningTasks;
...
  private void endOfTask() {
    // one less job to wait for
    numberOfRunningTasks--;
    // finished?
    if (numberOfRunningTasks == 0) {
      // end waiting
      cancelWaitingTasks();
      // the end of tasks is signalled to the daughter class
      notifyEndOfTasks(false);
    }
  }
...
  // classes filles -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • line 6: an asynchronous task has just completed. The counter for active tasks is decremented;
  • line 8: if there are no more active tasks, then the child thread has received all its responses;
  • line 10: the wait is canceled;
  • line 12: we notify the child fragment that all the tasks it launched have finished by calling its [notifyEndOfTasks] method. The parameter of this method indicates how the tasks ended—normally, or due to cancellation by the user or the code because an exception occurred. On line 12, we signal a normal end. Note that the child fragment does not need to keep track of which tasks are still active. Its parent class does that for it;

The [consumeThrowable] method is as follows:


  // asynchronous tasks
  protected int numberOfRunningTasks;
  private boolean runningTasksHaveBeenCanceled;
...
    // an asynchronous operation has thrown an exception
  // or an exception has occurred during the execution of an asynchronous operation
  private void consumeThrowable(Throwable th) {
    // th: the exception to be dealt with
    // 
    // log
    if (isDebugEnabled) {
      Log.d(className, "Exception reçue");
    }
    // cancel tasks already started
    cancelRunningTasks();
    // error messages are displayed
    showAlert(th);
  }
 
  // cancel tasks
  protected void cancelRunningTasks() {
    // log
    if (isDebugEnabled) {
      Log.d(className, "Annulation des tâches lancées");
    }
    // cancel all registered asynchronous tasks
    for (Subscription abonnement : abonnements) {
      abonnement.unsubscribe();
    }
    // we note the cancellation
    runningTasksHaveBeenCanceled = true;
    numberOfRunningTasks = 0;
    // end of wait
    cancelWaitingTasks();
    // the cancellation of tasks is reported to the daughter fragment
    notifyEndOfTasks(true);
}
 
...
  // classes filles -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • Line 3: The [consumeThrowable] method catches the exception that occurred;
  • line 15: all tasks that are still active are canceled;
  • line 17: the exception text is displayed;
  • lines 21–37: all tasks are canceled;
  • lines 27–29: all subscriptions are canceled;
  • line 31: a note is made that a cancellation occurred;
  • line 32: the task counter is reset to zero;
  • line 34: the wait is canceled;
  • line 36: the child fragment is notified that tasks have ended upon cancellation;

2.7.3.7. Fragment Lifecycle Management


  // life cycle --------------------------------------------------------
  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    // log
    if (isDebugEnabled) {
      Log.d(className, "onDestroyView");
    }
  }
 
  @Override
  public void onDestroy() {
    // parent
    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) {
...
}
  • Lines 2–20: The [onDestroyView, onDestroy] methods are included solely for logging purposes. These allow the developer to better understand the fragment lifecycle;

Saving the fragment when the device rotates is handled by the following methods: [setUserVisibleHint, onSaveInstanceState, saveState]:


  // fragment life cycle
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
...
 
@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // backup?
    if (this.isVisibleToUser && !isVisibleToUser) {
      // the fragment will be hidden - save it
      if (!saveFragmentDone) {
        saveState();
      }
    }
    // memory
    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));
    }
    // parent
    super.onSaveInstanceState(outState);
    // save fragment only if visible
    if (isVisibleToUser) {
      // perhaps the backup has already been made
      if (!saveFragmentDone) {
        saveState();
      }
      // restoration to be carried out in all cases
      session.setAction(ISession.Action.RESTORE);
    }
}
  • lines 6-19: the fragment is saved if it transitions from the visible state to the hidden state (line 11). The [setUserVisibleHint] method provides this information;
  • line 14: the save is performed by the private method in lines 21–23;
  • lines 25–41: When the device rotates, the [onSaveInstanceState] method is called. The fragment is saved under two conditions:
    • it is visible (line 34);
    • it has not yet been saved (line 36). It is possible that the [setUserVisibleHint] and [onSaveInstanceState] methods may not both execute when the fragment is visible, and that therefore managing the [saveFragmentDone] boolean is unnecessary. When in doubt, I chose to use it;
  • line 40: after saving comes restoring. Note that the next time the fragment needs to update itself, it will do so via a [RESTORE] operation;

Note the two moments when a fragment save is requested:

  1. when it transitions from the visible state to the hidden state;
  2. when the device rotates;

The private method [saveState] is as follows:


...
  private void saveState() {
    // tasks to cancel?
    if (numberOfRunningTasks != 0) {
      // cancel tasks
      cancelRunningTasks();
    }
    // save fragment state
    CoreState currentState = saveFragment();
    // the fragment has been visited
    currentState.setHasBeenVisited(true);
    // save menu status
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // session setting
    session.setCoreState(getNumView(), currentState);
    // backup done
    saveFragmentDone = true;
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }
 
 
...
  // classes filles -----------------------------------------------------
public abstract CoreState saveFragment();
 
protected abstract int getNumView();
  • lines 4-7: Device rotation may occur while asynchronous operations are in progress. Here, the decision is made to cancel them all. This is not a good decision for the user, who will have to make a new, potentially time-consuming request simply because they moved their phone or tablet or received a phone call. It is possible to maintain network connections through a save/restore cycle. However, the solutions are not straightforward, and I have decided not to cover them in this beginner’s course. The way forward is to establish these network connections via a fragment that has no attached UI and is not destroyed during the save/restore cycle. To do this, simply use the [Fragment.setRetainInstance(true)] instruction;
  • line 9: we ask the child fragment to save its state in a type derived from [CoreState] (line 31);
  • line 11: we note that the fragment has been visited. This information is useful. When a fragment is visited for the first time, its update may differ from subsequent ones because it has no previous state in the session;
  • line 13: we save the menu’s state, which will allow us to restore it automatically;
  • line 15: this current state is saved in the session. In the session, states are grouped by view/fragment, each having a state. The view number is provided by the child fragment (line 33);
  • line 17: we note that the fragment has been saved. This is because two methods may call the [saveState] method, and it is unnecessary to perform two saves;

The view associated with the fragment is regenerated by the following method:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // parent
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // the view must be restored
    viewHasToBeInitialized = true;
}

In the lifecycle, the [onActivityCreated] method is executed immediately after the [onCreateView] method. Calling the latter method indicates that the view associated with the fragment must be rebuilt. We simply note this on line 10.

2.7.3.8. Updating the fragment

Updating the fragment is the last operation performed on the fragment before it becomes visible and waits for user input. It is handled by the following code:


  // fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment life cycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
  // update Fragment ----------------------------------------------------------------------------------
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "onCreateOptionsMenu");
    }
    // memory
    this.menu = menu;
    // retrieve # menu options if not already done
    if (fragmentHasToBeInitialized) {
      // retrieve the # menu options
      getMenuOptionsStates(menu);
      // activity
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = (Session) this.mainActivity.getSession();
    }
    // retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
    previousState = session.getCoreState(getNumView());
    // multi-stage update of the daughter fragment
    // step 1 - is this your 1st visit?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
  ...
    } else {
      // this is not the 1st visit
      // step 2: does the fragment need to be initialized?
      ...
      // step 3: should the view be initialized?
      ...
    }
    // step 4: a submit, a browse, a restore?
    ...
 
    // step 5: terminal updates ----------------------
...
  }
...
  // classes filles -----------------------------------------------------
  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();
  • line 19: the [onCreateOptionsMenu] method is used to update the fragment. For this reason, the fragment must have a menu, even if it is empty. When this method is executed, the fragment has been associated with its view and activity and is also visible;
  • line 25: the menu passed as a parameter (line 22) to the method is stored;
  • lines 27–34: if the fragment needs to be initialized:
    • line 29: the states of the menu options are stored in the [menuOptionsStates] array from line 3;
    • line 31: the activity is stored as an instance of the Android [Activity] type;
    • line 32: the activity is stored as an instance of the [IMainActivity] interface;
    • line 33: the session is stored. The type cast is necessary because the method [mainActivity.getSession()] returns a type [ISession];
  • line 36: the fragment’s previous state is retrieved from the session. If this is the first visit to the fragment, only the boolean [previousState.hasBeenVisited] is relevant;
  • lines 39–44: code executed when this is the first visit to the fragment. In this case, its previous state is not relevant;
  • lines 44–50: code executed when this is not the first visit to the fragment;
  • lines 46–47: code executed if the fragment’s constructor has been called (fragmentHasToBeInitialized == true);
  • lines 48-49: code executed if the view associated with the fragment has been rebuilt (viewHasToBeInitialized==true);
  • lines 51-52: code executed depending on the current action (SUBMIT, NAVIGATION, RESTORE);
  • lines 54-55: code always executed;

The five steps of the update are as follows:

step 1


  // fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment life cycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
 
    // retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
    previousState = session.getCoreState(getNumView());
    // multi-stage update of the daughter fragment
    // step 1 - is this your 1st visit?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
      // fragment and view initialization
      initFragment(null);
      initView(null);
      // raz previousState for the suite
      previousState = null;
    } else {
      // this is not the 1st visit
...
 
  protected abstract void initFragment(CoreState previousState);
 
protected abstract void initView(CoreState previousState);
  • line 19: the fragment's previous state is retrieved from the session;
  • lines 22–31: code executed if the fragment has never been visited;
  • line 27: the child class is asked to initialize the fragment. The parameter of the [initFragment] method on line 35 is the fragment’s previous state. Here, null is passed to indicate to the child fragment that this is the first visit;
  • line 28: the child class is asked to initialize the view associated with the fragment. The parameter of the [initView] method on line 37 is the fragment’s previous state. Here, null is passed to indicate to the child fragment that this is the first visit;
  • line 30: we set the previous state to null for the steps that follow;

Steps 2 and 3


// fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment life cycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
 
     // we retrieve the previous fragment state (the very 1st time, only the boolean hasBeenVisited represents anything)
    previousState = session.getCoreState(getNumView());
    // multi-stage update of the daughter fragment
    // step 1 - is this your 1st visit?
    if (!previousState.getHasBeenVisited()) {
...
    } else {
      // ce n'is not the 1st visit
      // step 2: does the fragment need to be initialized?
      if (fragmentHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation fragment");
        }
        // girl fragment
        initFragment(previousState);
      }
      // step 3: should the view be initialized?
      if (viewHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation vue");
        }
        // girl fragment
        initView(previousState);
      }
    }
 
...
 
  protected abstract void initFragment(CoreState previousState);
 
protected abstract void initView(CoreState previousState);
  • lines 24–42: executed when this is not the first visit to the fragment;
  • lines 27–33: if the fragment has just been reconstructed, it is reinitialized by calling the [initFragment] method of the child class (lines 32, 46). The previous state of the fragment is passed to it;
  • lines 35–51: if the view associated with the fragment needs to be initialized or reset, the child fragment is asked to do so (lines 40, 48). Again, the fragment’s last known state is passed to it;

Step 4


// fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment life cycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
 
    // retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
    previousState = session.getCoreState(getNumView());
    // multi-stage update of the daughter fragment
 ...
 
    // step 4: a submit, a browse, a restore?
    // log
    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();
      }
    }
    // action in progress
    ISession.Action action = session.getAction();
    switch (action) {
      case SUBMIT:
        if (isDebugEnabled) {
          Log.d(className, "updateOnSubmit");
        }
        // girl fragment
        updateOnSubmit(previousState);
        break;
      case NAVIGATION:
        if (isDebugEnabled) {
          Log.d(className, "updateForNavigation");
        }
        if (previousState != null) {
          // catering menu
          setMenuOptionsStates(previousState.getMenuOptionsState());
          // girl fragment
          updateOnRestore(previousState);
        } else {
          // 1st visit - nothing to do
        }
        break;
      case RESTORE:
        // restoration
        if (isDebugEnabled) {
          Log.d(className, "updateOnRestore");
        }
        // menu restoration (previousState cannot be null)
        setMenuOptionsStates(previousState.getMenuOptionsState());
        // girl fragment
        updateOnRestore(previousState);
        break;
    }
....
  protected abstract void updateOnSubmit(CoreState previousState);
 
protected abstract void updateOnRestore(CoreState previousState);
  • lines 34–66: we process the current action, which can be one of the following three:
    • RESTORE: We are restoring the fragment after a device rotation;
    • NAVIGATION: We are returning to the fragment, intending to find it in the state we left it in the last time we used it;
    • SUBMIT: all other cases;
  • line 34: retrieve the current action;
  • lines 36–42: for a SUBMIT-type action, we call the [updateOnSubmit] method of the child fragment (lines 41, 68), passing it the fragment’s last known state;
  • lines 43–55: for a NAVIGATION-type action;
  • lines 47–54: we want to restore the fragment to its last known state. The NAVIGATION operation may coincide with a first visit. This would be the case, for example, in a tabbed application: if I switch from tab 1 to tab 4:
    • I must initialize the fragment for tab 4 if this is the first visit;
    • restore the fragment of tab 4 to its previous state if it is not the first visit;
  • lines 52–54: do nothing if it is the first visit. The child method [initView(CoreState previousState)] will handle this initialization. The first visit is identified by the condition [previousState == null];
  • line 49: if this is not the first visit to the fragment, restore its menu;
  • line 51: we ask the child class to update itself by calling the method on line 70. We pass it the fragment’s previous state so it can do its job;
  • lines 56–66: in the case of a fragment restore operation, we do the same thing as in the case of navigation outside the first visit;

Step 5


// fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment life cycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
 
    // step 5: terminal updates ----------------------
    // we've changed our view
    session.setPreviousView(getNumView());
    // more action in progress
    session.setAction(ISession.Action.NONE);
    // when leaving this fragment, it must be saved
    saveFragmentDone = false;
    // as long as the fragment has not been rebuilt, it does not need to be initialized
    fragmentHasToBeInitialized = false;
    // as long as the view has not been rebuilt, it does not need to be initialized
    viewHasToBeInitialized = false;
    // returns to normal tab selection operation
    session.setNavigationOnTabSelectionNeeded(true);
 
    // the fragment is notified that the view is ready
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • lines 18–30: when we reach this point, the fragment has been initialized and is ready to be displayed. We then reset all the indicators used in the fragment’s lifecycle management to their initial state;
  • line 20: the view has changed; this is noted in the session;
  • line 22: there are no more actions in progress;
  • line 24: when we exit the currently displayed fragment, we will need to save it upon exit;
  • line 26: the fragment no longer needs to be reconstructed. This flag will be set to true when the fragment’s constructor is executed again;
  • line 28: the view associated with the fragment no longer needs to be initialized. This flag will be set to true again when the [onActivityCreated] method is executed again;
  • line 30: the fragment may be displayed in a tabbed application. In this case, when the user clicks on one of the tabs, a fragment change must occur;
  • line 36: the child class is notified that the fragment is ready. It can use the [notifyEndOfUpdates] method to perform updates that would need to be done in any case, launch an asynchronous operation to fetch new data, etc.

2.7.4. An example of a fragment

  

We have included a fragment example in the [client-android-skel] project to show the reader the typical structure of a fragment in an application based on this project.

The [DummyFragment] class is as follows:


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 {
 
  // fields inherited from parent class -------------------------------------------------------
 
  // debug mode
  //-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // class name
  //-- protected String className;
  // asynchronous tasks
  //-- protected int numberOfRunningTasks;
  // activity
  //-- protected IMainActivity mainActivity;
  //-- protected Activity activity;
  // session
  //-- protected Session session;
 
  // methods inherited from the parent class -------------------------------------------------------
 
  // display menu options
  //-- protected void setAllMenuOptionsStates(boolean isVisible) {
  //-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
  // management of waiting for the end of a series of asynchronous tasks
  //-- protected void beginRunningTasks(int numberOfRunningTasks) {
  //-- protected void cancelWaitingTasks() {
  // asynchronous task execution with RxAndroid
  //-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
  // cancel tasks
  //-- protected void cancelRunningTasks() {
  // exception alert display
  //-- protected void showAlert(Throwable th) {
  // message list display
  //-- protected void showAlert(List<String> messages) {
 
  // methods imposed by the parent class -------------------------------------------------------
 
  @Override
  public CoreState saveFragment() {
    // save the fragment
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
  }
 
  @Override
  protected int getNumView() {
    // return the fragment number in the table of fragments managed by the activity (cf MainActivity)
    return 0;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // the fragment becomes visible and has undergone construction in this or a previous stage
    // this happens on application startup and every time the Android device is rotated
    // is necessarily followed by the execution of [initView]
    // the fields of the fragment that has been rebuilt must be initialized
    // previousState is the fragment's last save - is null if this is the fragment's 1st visit
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // the fragment becomes visible and the associated view has been reconstructed in this or a previous step
    // this happens every time [initFragment] is executed and every time the fragment leaves the adjacency of the displayed fragment
    // initialize the components of the view that has been rebuilt
    // previousState is the fragment's last save - is null if it's the fragment's 1st visit
 
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // is executed after [initFragment, initView] if these methods are executed
    // the view will be displayed after an operation of type SUBMIT
    // the fragment and the associated view usually have to be initialized from the session
    // previousState is the fragment's last save - is null if it's the fragment's 1st visit
    // there's nothing to be done if the fragment can't be reached by a SUBMIT operation
    // if the fragment can be reached by SUBMIT operations from different fragments, the previous view can be known by [session.getPreviousView]
    // if the fragment can be reached by several SUBMIT operations from the same fragment, then a flag must be set to differentiate between the different types of SUBMIT from this fragment
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // is executed after [initFragment, initView] if these methods are executed
    // the view will be displayed after an operation of type RESTORE or NAVIGATION
    // previousState is the fragment's last backup - never null
    // restore the view to its previous state
 
  }
 
  @Override
  protected void notifyEndOfUpdates() {
    // comes after methods [updateOnSubmit, updateOnRestore]
    // when we're there, the view has been built and initialized
    // there's often nothing to do here, but you can also factor in actions that need to be done no matter how you arrive at this view
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // called when asynchronous tasks launched by the fragment are either completed or cancelled
    // these two cases can be differentiated using parameter runningTasksHaveBeenCanceled
    // the view generally needs to be reset to a different state from the one it had while waiting for responses from asynchronous tasks
 
  }
}

The [DummyFragment] class may not have a state. Here, we have included one to remind us of what is expected within it:


package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class DummyFragmentState extends CoreState {
  // fragment status [DummyFragment]
  // set only serializable fields to jSON
  // put the annotation @JsonIgnore on the others, but it's hard to see what use they could be
  // don't forget the getters/setters - they are used for serialization/deserialization
}

To illustrate the use of the [client-android-skel] project, we will first use simple examples before moving on to a more comprehensive case study.

2.8. Illustrative Exercises

We’ll start by refactoring examples that have already been written.

2.8.1. Example-17B

We’ll revisit Example 17 from Section 1.18. This is an app with a single fragment, no asynchronous tasks, and no tabs. We’ll examine it to see how it behaves when the device is rotated. We’ll enter the following:

Image

Then, in [1], we rotate the device twice. The new view is as follows:

Image

If we compare the views, everything has been preserved except for list [2], which is now empty.

Furthermore, if you click the [Submit] button, a dialog box appears showing the entries made on the form. If you rotate the device at that moment, the dialog box disappears.

Therefore, during a rotation, we will need to regenerate:

  • the drop-down list and its selected item;
  • the dialog box if it was displayed during the rotation;

2.8.1.1. The [Example-17B] project

We duplicate the [client-android-skel] project into examples/Example-17B. Then we load the new project [1]:

  • in [2-3], in the [behavior] folder, we paste the fragment [Vue1Fragment] from the [Example-17] project;
  • in [4-5], in the [layout] folder of [Example-17B], we paste the [vue1.xml] view from [Example-17]. This is the view associated with the fragment;
  • in [6], the [values] folder from [Example-17B] is replaced by the [values] folder from [Example-17];

We will change the top margin of the [vue1.xml] view to 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"/>

At this point, we can attempt an initial compilation to check for errors. The first errors reported stem from package imports that have been moved. We fix them (Ctrl-Shift-O). Other errors, such as , arise because the view [Vue1Fragment] does not implement all the methods required by its parent class [AbstractParent]:

Image

Generate the missing methods (Alt-Enter).

Another compilation error reported is as follows:

Image

We fix this in the module’s [build.gradle] file (line 20 below):

 

At this point, we can recompile to see the remaining errors. The only error reported is on the [Vue1Fragment.updateFragment] method:

 

You must remove the [@Override] annotation from line 135. There are now no more errors. We will use this as a starting point to modify the project.

2.8.1.2. The state of the [Vue1Fragment] fragment

The [Vue1Fragment] fragment needs to save information when the device rotates so that it can be fully restored. We create a [Vue1FragmentState] class for this:

  

For now, this class is empty:


package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class Vue1FragmentState extends CoreState {
 
}

2.8.1.3. Project Customization

  

The [custom] folder contains architecture elements that can be customized by the developer.

The constants for the [IMainActivity] interface will be as follows:


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // constant application -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 0;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = false;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 1;
 
}
  • lines 24–31: The application does not use its [DAO] layer here. These constants will not be used;
  • line 34: a fragment adjacency of 1, which is the default value. Since the application has only one fragment (line 43), this value is irrelevant;
  • lines 39-40: since there are no operations with the [DAO] layer, there is no need for a placeholder image;
  • line 37: this is not a tabbed application;
  • line 43: there is only one fragment;

The [Session] class is as follows:


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
 
}

It is empty. Indeed, since there is only one fragment, there is no need to provide for inter-fragment communication using a session.

Finally, the [CoreState] class is as follows:


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 {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • Lines 11–13: We need to list all classes derived from [CoreState] that store the state of the various fragments. Here, there is only one (line 12);

2.8.1.4. The [MainActivity]

The [MainActivity] activity currently looks like this:


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 {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
    // todo: we continue the initializations started by the parent class
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    // todo: define fragments here
    return new AbstractFragment[0];
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    // todo: define fragment titles here
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // todo: tabbed navigation - define the view to be displayed when tab no. [position] is selected
  }
 
  @Override
  protected int getFirstView() {
    // todo: define the number of the first view (fragment) to be displayed
    return 0;
  }
}

The comments [//todo] indicate what the developer needs to do. The [MainActivity] class evolves as follows:


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 {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    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;
  }
}

Only the method in lines 41–44 needs to be modified. It must return the array of the app’s fragments. On line 43, don’t forget to add the underscore after the fragment name.

2.8.1.5. The fragment state [FragmentState]

Following the rotation tests performed on the [Example-17] project, we decide to store the following elements of the fragment:

  • the list of values in the drop-down list;
  • the position of the selected item in this list;
  • the message displayed by the dialog box if it is present at the time of rotation;

The [Vue1FragmentState] class will be as follows:

  

package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
import java.util.List;
 
public class Vue1FragmentState extends CoreState {
 
  // drop-down list values
  private List<String> list;
  // the item selected from the drop-down list
  private int listSelectedPosition;
  // the message displayed in the
  private String message;
 
  // getters and setters
...
}

2.8.1.6. The [AbstractFragment] fragment

Currently, the fragment's lifecycle is managed by two methods (lines 6 and 32):


// drop-down list
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;
 
  @AfterViews
  void afterViews() {
    // check the first button
    radioButton1.setChecked(true);
    // the calendar
    datePicker1.setCalendarViewShown(false);
    // on seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
 
      public void onStopTrackingTouch(SeekBar seekBar) {
      }
 
      public void onStartTrackingTouch(SeekBar seekBar) {
      }
 
      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // the drop-down list
    list = new ArrayList<>();
    list.add("list 1");
    list.add("list 2");
    list.add("list 3");
  }
...
  protected void updateFragment() {
    // initialize drop-down list adapter
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    dropDownList.setAdapter(dataAdapter);
  }

The code for these two methods will be moved into the methods defined by the [AbstractFragment] class as follows:


// fragment lifecycle management ---------------------------------------------------------------------
  @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) {
    // 1st visit?
    if (previousState == null) {
      // create drop-down list values
      list = new ArrayList<>();
      list.add("list 1");
      list.add("list 2");
      list.add("list 3");
    } else {
      // returns values from the drop-down list
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      list = state.getList();
      // and the
      message = state.getMessage();
    }
    // initialize drop-down list adapter
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // the calendar
    datePicker1.setCalendarViewShown(false);
    // on seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
 
      public void onStopTrackingTouch(SeekBar seekBar) {
      }
 
      public void onStartTrackingTouch(SeekBar seekBar) {
      }
 
      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // initialize drop-down list adapter
    dropDownList.setAdapter(dataAdapter);
    // 1st visit?
    if (previousState == null) {
      // check the first button
      radioButton1.setChecked(true);
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // seekbar value
    seekBarValue.setText(String.valueOf(seekBar.getProgress()));
    // item selected from drop-down list
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    dropDownList.setSelection(state.getListSelectedPosition());
    // visible dialogue?
    if (message != null) {
      // we display it
      showMessage();
    }
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
 
}
  • lines 2–9: the [saveFragment] method must place the fragment’s elements to be saved into a class derived from [CoreState] and return an instance of that class;
  • lines 11–14: the [getNumView] method must return the fragment number. Here, there is only one fragment, whose number is 0;
  • lines 16–34: The [initFragment] method must initialize the fragment’s fields. It receives the fragment’s previous state. If [previousState] is null, then this is the first visit;
  • lines 19–25: On the first visit, the values for the dropdown list are created;
  • lines 26–30: if this is not the first visit, the fragment’s [list, message] fields are restored from the previous state;
  • lines 33-34: initialization of the fragment’s [dataAdapter] field. This is the data source for the drop-down list;
  • lines 37–62: the [initView] method is used to initialize the visual interface components. It receives the previous state [previousState] as a parameter. If [previousState == null], then this is the first visit;
  • Here, we see what was previously in the [@AfterViews] method;
  • lines 57–61: on the first visit, we ensure that the first radio button is selected;
  • lines 64–67: the [updateOnSubmit] method is executed when the current action is [SUBMIT]. Here, there is no inter-fragment navigation and therefore no current action;
  • lines 69–81: the [updateOnRestore] method is executed when the current action is [NAVIGATION] or [RESTORE]. Here, there is no inter-fragment navigation and therefore no possible [NAVIGATION] action;
  • line 72: we recalculate (not restore) the value of the TextView seekBarValue. This is because, during rotations, its value was sometimes lost;
  • lines 74–75: The list is positioned on the item that was selected before the rotation. Without this, the list would default to its first item;
  • lines 76-80: the dialog box is displayed again if the message from the previous state is not null. We will return to the [showMessage] method (line 79);
  • lines 83–86: the [notifyEndOfUpdates] method is the last method called by the parent class before leaving the child fragment alone. Here, there is nothing to do;
  • lines 88–91: the [notifyEndOfTasks] method signals the end of asynchronous tasks launched by the fragment. Here, there are none;

The dialog box is restored as follows:


  // dialog box message
  private String message;
...
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // list of messages to display
    List<String> messages = new ArrayList<>();
    ...
    // display
    doAfficher(messages);
  }
 
  private void doAfficher(final List<String> messages) {
    // poster text is created
    StringBuilder texte = new StringBuilder();
    for (String message : messages) {
      texte.append(String.format("%s\n", message));
    }
    // the message is memorized
    message = texte.toString();
    // we display it
    showMessage();
  }
 
  private void showMessage() {
    // we display it
    new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        // message reset
        message = null;
      }
    }).show();
}

When the user submits the form, the [doValider] method (line 5) builds a list of messages, which it then displays (line 10) in the dialog box.

  • lines 14–20: The list of messages is concatenated into a single message, which is stored in line 2;
  • lines 25–33: this is the message displayed by the dialog box, and it is the same message that the [updateOnRestore] method displays;
  • line 27: the second parameter of the [setNeutralButton] method is the method executed when the user clicks the [Close] button in the dialog box;
  • line 31: when the dialog box closes, the message is set to null to indicate that the dialog box is no longer present;

2.8.1.7. Tests

Readers are invited to test this project and verify that the fragment is preserved after one or more successive rotations.

2.8.2. Example-23: Weather Client

Some websites provide weather information in the form of JSON strings. Here is an example:

Image

The URL is in the form: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID} where:

  • city: the city for which you want the weather, here Angers;
  • country: the country of the city, in this case France (fr);
  • APPID: a key obtained by registering on the site [https://home.openweathermap.org/users/sign_up];

2.8.2.1. The Project

  

The project was built based on the [client-android-skel] project. It has the following characteristics:

  • it has only one fragment whose state does not need to be maintained;
  • it makes asynchronous requests;

2.8.2.2. Project Customization

  

The [IMainActivity] interface allows you to specify certain project characteristics:


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // constant application -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 5000;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = true;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 1;
 
}
  • Lines 25, 28, 31, 40: characteristics of the [DAO] layer. Line 31: Basic authentication is not required;
  • line 34: fragment adjacency. Here, this constant is irrelevant since there is only one fragment;
  • line 37: this is not a tabbed application;
  • line 43: there is only one fragment;

The [CoreState] class that stores the state of the fragments will be as follows:


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)
// todo: add subclasses of [CoreState] here
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • lines 10–13: there is nothing to declare since this application has only one fragment whose state is not saved;

The [Session] class is as follows:


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}

It is empty because there is no inter-fragment communication in this application.

2.8.2.3. The [DAO] layer

  

In the [DAO] layer, three classes must be customized:

  • the IDao interface;
  • its Dao implementation;
  • the WebClient interface for communication with the web server / JSON;

The [WebClient] interface will be as follows:


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);
 
  // weather service
  @Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
  String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
  • lines 18-19: the URL of the weather service. Note that this is relative to the client’s root URL (RestClientRootUrl, line 12). Here, this root URL will be [http://api.openweathermap.org/];

The [IDao] interface will be as follows:


package client.android.dao.service;
 
import rx.Observable;
 
public interface IDao {
  // Web service url
  void setUrlServiceWebJson(String url);
 
  // user
  void setUser(String user, String mdp);
 
  // customer timeout
  void setTimeout(int timeout);
 
  // basic authentication
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  //  weather service
  Observable<String> getWeatherForecast(String city, String country, String APPID);
}
  • Note that the methods in lines 6–22 are included by default in the IDao interface of the [client-android-skel] project;
  • Line 25: The [getWeatherForecast] method retrieves the JSON string for the weather in the city [city] of the country [country]. The third parameter is the key obtained from the website [https://home.openweathermap.org/users/sign_up];

The [IDao] interface is implemented by the following [Dao] class:


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 {
 
  // web service customer
  @RestService
  protected WebClient webClient;
  // safety
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // on RestTemplate
  private RestTemplate restTemplate;
  // factory du RestTemplate
  private SimpleClientHttpRequestFactory factory;
  // timeout
  private int timeout;
 
  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // we build the restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // set the jSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // set the restTemplate of the web client
    webClient.setRestTemplate(restTemplate);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    // set the URL of the web service
    webClient.setRootUrl(url);
  }
 
  @Override
  public void setUser(String user, String mdp) {
    // the user is registered in the interceptor
    authInterceptor.setUser(user, mdp);
  }
 
  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // memory
    this.timeout = timeout;
    // factory configuration
    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));
    }
    // authentication interceptor?
    if (isBasicAuthentificationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }
 
 
  // méthodes privées -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // service météo ---------------------------------------------------------
  @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));
    }
    // result
    return getResponse(new IRequest<String>() {
      @Override
      public String getResponse() {
        return webClient.getWeatherForecast(city, country, APPID);
      }
    });
  }
}
  • Note that lines 17–90 are included by default in the [Dao] class of the [client-android-skel] project. You just need to add the implementation methods for the [IDao] interface, specific to the application (line 92);
  • lines 93–105: implementation of the [getWeatherForecast] method. This is very simple and takes up 6 lines, lines 100–105;
  • line 100: the [getResponse] method is a method of the parent class [AbstractDao]. It expects a parameter of type [IRequest<T>], where T is the type of the expected response from the server; here, it is a String since we are expecting a JSON string. The type T of [IRequest<T>] must be the type T of the [Observable<T> getWeatherForecast] method;
  • the [IRequest<T>] interface has only one method: getResponse. Its role is to provide the response of type T that the [Observable<T> getWeatherForecast] method must return;
  • line 103: it is the [WebClient] interface that provides this response. We pass it the three parameters received on line 94. For this reason, these must have the final attribute;

2.8.2.4. The [MainActivity]

  

The [MainActivity] activity is as follows:


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 {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
 
  // methods parent class -----------------------
  @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);
  }
}
  • Note that lines 15–55 are included by default in the [client-android-skel] project. You just need to customize them;
  • Lines 37–40: the fragment array. There is only one here;
  • Lines 43–46: No fragment titles are required;
  • lines 48–50: no tabs here;
  • Lines 52–55: The first view to display is view #0, that of [MeteoFragment];
  • lines 58–61: implementation of the [IDao] interface. Here, there is nothing to do other than delegate the work to the [DAO] layer on line 21;

2.8.2.5. The [MeteoFragment] fragment

  

The [MeteoFragment] queries the weather web service / JSON. Its skeleton is as follows:


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 {
...
}
  • Line 14: The view [res/layout/meteo_fragment.xml] is as follows:

<?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>

The view displays only the text from line 10;

  • line 15: the menu [res / menu / menu_meteo.xml] is as follows:

<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>
  • lines 10-12: this menu option is used to request the weather for a city;
  • lines 14-15: this menu option is used to cancel the request if it is in progress;
  • lines 16-18: this menu option closes the application;

The complete code for the fragment is as follows:


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 {
 
  // local data
  private int nbReponsesRecues;
 
  // gestion des événements ---------------------------------------------------------------------------------------
  // cities whose weather we want
  final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
 
  @OptionsItem(R.id.actionMeteo)
  protected void doMeteo() {
    // his country
    String country = "fr";
    // get a API login by creating an account [https://home.openweathermap.org/users/sign_up]
    String APPID = "xyz";
    // URL web service / jSON
    mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
    // start waiting for [paysDeLoire.length] asynchronous tasks
    beginWaiting(paysDeLoire.length);
    // number of responses received
    nbReponsesRecues = 0;
    // asynchronous calls are made in parallel
    for (String city : paysDeLoire) {
      // weather
      executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
        @Override
        public void call(String response) {
          // exploiting the answer
          consumeResponse(response);
          // a + response
          nbReponsesRecues++;
        }
      });
    }
  }
 
  // exploitation server response
  private void consumeResponse(String response) {
    // log
    Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
  }
 
  // start of waiting
  protected void beginWaiting(int numberOfRunningTasks) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "beginWaiting");
    }
    // parent
    beginRunningTasks(numberOfRunningTasks);
    // the [Cancel] option is displayed
    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();
    // displaying results
    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();
  }
 
  // private methods -----------------------------------
  private void initMenu() {
    if (isDebugEnabled) {
      Log.d(className, "initMenu");
    }
    // menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
 
  // life cycle management ---------------------------------------------------------------------------------------
...
}
  • lines 25-50: handling the click on the [Weather] menu option;
  • line 32: construction of the web service URL / JSON for the weather service. This is then passed to the [DAO] layer via the activity;
  • line 34: we start the wait. We pass the number of tasks to be launched so that the parent class can notify us when they are complete. Here, there are five tasks because we are requesting the weather for the five cities listed on line 23;
  • line 16: we count the number of responses received so we can display it;
  • lines 38–50: we loop through the cities for which we want the weather;
  • line 40: we will make 5 HTTP requests in parallel;
  • line 40: we ask the parent class [AbstractParent] to query the web service / JSON;
  • lines 40–48: the [executeInBackground] method expects two parameters:
    • line 40: the process to be observed and executed is provided by the [mainActivity.getWeatherForecast] method;
    • lines 40–48: the [Action1] instance to be executed when the response from the asynchronous service is received. The type T of [Action1<T>] must be the type T of the result of the [getWeatherForecast] method;
  • line 44: a response has been received. It is passed to the [consumeResponse] method on line 53;
  • line 46: the counter for received responses is incremented;
  • lines 53–56: consuming a JSON response from the weather service;
  • line 55: we simply log the JSON string;
  • lines 59–72: code executed before launching the asynchronous tasks;
  • line 65: we pass the number of tasks to be executed to the parent class [AbstractParent]. This allows it to notify us when they are all finished;
  • lines 67–70: preparing the menu for a wait. We keep only the [Actions/Cancel] option, which will allow the user to cancel the launched tasks;
  • lines 74–92: code executed when the parent class notifies us that all launched tasks are complete;
  • line 77: we reset the menu to its initial state. The [initMenu] method (lines 95-102) displays the menu with all its options except the [Actions/Cancel] option, which is hidden;
  • lines 80–91: the number of responses received is displayed;

Clicking the [Cancel] menu option is handled by the following code:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchronous tasks are cancelled
    cancelRunningTasks();
}
  • line 7: we ask the parent class to cancel the tasks that are still active;

Clicking the [Finish] menu option is handled by the following code:


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminer() {
    // we stop everything
    System.exit(0);
}

The fragment's lifecycle is managed by the following methods:


  // life cycle management ---------------------------------------------------------------------------------------
 
  @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) {
    // 1st visit?
    if (previousState == null) {
      initMenu();
    }
  }
 
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
 
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
}
  • lines 3-6: used to store the fragment's state in a class derived from [CoreState]. If the fragment has no state to store, as in this case, we simply return an instance of [CoreState]. Do not return null, as this would eventually cause a crash;
  • lines 8-11: must return the view ID. Here, the [MeteoFragment] has ID 0;
  • lines 13–16: used to initialize the fragment once it has been constructed (previousState == null) or reconstructed (previousState != null). Here, there is nothing to do. The only field that can be initialized is the following:

  // villes dont on veut la météo
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

but it initializes itself;

  • lines 18–24: are used to initialize the view associated with the fragment once it has been constructed (previousState == null) or reconstructed (previousState != null);
  • lines 21-23: if this is the first visit to the fragment, its menu is initialized to hide the [Cancel] option;
  • lines 27–30: called if navigation to the fragment involved a [SUBMIT] action. Here, there is no inter-fragment navigation since there is only one fragment;
  • lines 32-35: called during a save/restore cycle due to device rotation or another reason. Here, since no state has been saved, there is nothing to do;
  • lines 37–40: called when all previous updates have been completed. Here, there is nothing to do;

2.8.2.6. Tests

We now run the example:

Image

Image

The logs are as follows:


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
  • lines 32-36: JSON responses are obtained on I/O threads
  • lines 37-41: the fragment retrieves the 5 responses on the UI thread;

Now, we make the request with an incorrect API ID:


    String APIID = "";

Image

The logs are then as follows:


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"]]
  • lines 3-6, 10: the 5 HTTP calls generated 5 exceptions;
  • line 7: the [MeteoFragment] fragment receives the first exception. It will then cancel all tasks;

Now let’s set a 5-second timeout [IMainActivity.DELAY] and cancel the operation. The logs are then as follows:


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]]
  • line 3: cancellation request;
  • line 4: the wait is canceled because a cancellation occurred;
  • lines 6–10: Canceling the tasks causes an exception on each of the five task threads. The exception type depends on the applications. The exception here is [java.lang.InterruptedException] because the tasks were interrupted while executing the [Thread.sleep(delay)] instruction, which causes them to wait artificially for [delay] milliseconds;

2.8.3. Example-16B

Here we refactor Example 16 from Section 1.17. It presents a fragment that makes asynchronous calls to a random number server. Let’s see how it behaves during a device rotation:

Image

  • In [1], the device is rotated twice;

Image

We can see that we’ve lost all the error messages. We’ll try to improve this.

2.8.3.1. The Example-16B Project

We copy the [client-android-skel] project into the [examples/Example-16B] project, then load the new project:

  

From the initial project [Example-16], we copy the following elements into [Example-16B]:

  • the file [res/layout/vue1.xml], the folder [res/values]:
  

We will change the top margin of the [vue1.xml] view to 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" />
  • the fragment [View1Fragment]:
  
  • the class [DAO / service / Response]:
  

At this stage, we can attempt an initial compilation:

  • The first type of error involves imports. Some classes have been moved to different packages during the migration to [Example-16B]. We start by fixing these errors;
  • A second type of error is reported on the [Vue1Fragment] class because it does not implement the methods required by the parent class [AbstractParent]. We automatically generate these methods;

We attempt a second compilation:

  • all remaining errors are now concentrated in the [Vue1Fragment] class, the class that will undergo the most changes;

2.8.3.2. Creating a state for the [Vue1Fragment] fragment

We have seen that certain information from the fragment will need to be saved during a rotation in order to restore the fragment to its state prior to the rotation. We therefore create a [Vue1FragmentState] state, which is empty for now:

  

package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class Vue1FragmentState extends CoreState {
 
}

2.8.3.3. Project Customization

  

The [IMainActivity] interface allows you to specify certain project characteristics:


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // constant application -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 5000;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = true;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 1;
 
}
  • lines 25, 28, 31, 40: characteristics of the [DAO] layer. Basic authentication is not required;
  • line 34: fragment adjacency. Here, this constant is irrelevant since there is only one fragment;
  • line 37: this is not a tabbed application;
  • line 43: there is only one fragment;

The [CoreState] class that stores the state of the fragments will be as follows:


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 {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • Line 12: We declare the fragment state class [Vue1Fragment];

The [Session] class is as follows:


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}

It is empty because there is no inter-fragment communication in this application.

2.8.3.4. The [DAO] layer

  

In the [DAO] layer, three classes must be customized:

  • the IDao interface;
  • its Dao implementation;
  • the WebClient interface for communicating with the web server / JSON;

The [Response] class comes from the [Example-16] project, which uses it:


package client.android.dao.service;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}

The [WebClient] interface will be as follows:


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 random number in the range [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
 
}
  • Lines 18–19: The URL for the random number service. Note that this URL is relative to the client’s root URL (RestClientRootUrl, line 12). Here, the root URL is [http://localhost:8080];

The [IDao] interface will be as follows:


package client.android.dao.service;
 
import rx.Observable;
 
public interface IDao {
  // Web service url
  void setUrlServiceWebJson(String url);
 
  // user
  void setUser(String user, String mdp);
 
  // customer timeout
  void setTimeout(int timeout);
 
  // basic authentication
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // random number service
  Observable<Response<Integer>> getAlea(int a, int b);
 
}
  • Note that the methods in lines 6–22 are present by default in the IDao interface of the [client-android-skel] project;
  • Line 25: The [getAlea] method returns a random number in the range [a,b]. This number is returned in a [Response<Integer>] response, where the random number is contained in the [body] field of that type;

The [IDao] interface is implemented by the following [Dao] class:


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 {
 
  // web service customer
  @RestService
  protected WebClient webClient;
  // safety
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // on RestTemplate
  private RestTemplate restTemplate;
  // factory du RestTemplate
  private SimpleClientHttpRequestFactory factory;
 
  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // we build the restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // set the jSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // set the restTemplate of the web client
    webClient.setRestTemplate(restTemplate);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    // set the URL of the web service
    webClient.setRootUrl(url);
  }
 
  @Override
  public void setUser(String user, String mdp) {
    // the user is registered in the interceptor
    authInterceptor.setUser(user, mdp);
  }
 
  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // factory configuration
    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));
    }
    // authentication interceptor?
    if (isBasicAuthentificationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // méthodes privées -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // random number service
  @Override
  public Observable<Response<Integer>> getAlea(final int a, final int b) {
    // web client execution
    return getResponse(new IRequest<Response<Integer>>() {
      @Override
      public Response<Integer> getResponse() {
        return webClient.getAlea(a, b);
      }
    });
  }
 
}
  • Note that lines 17–85 are included by default in the [Dao] class of the [client-android-skel] project. You just need to add the methods to implement the [IDao] interface;
  • lines 88–97: implementation of the [getAlea] method. This is very simple and takes up 6 lines, lines 91–96;
  • line 91: the [getResponse] method is a method of the parent class [AbstractDao]. It expects a parameter of type [IRequest<T>], where T is the type of the expected response, in this case a Response<Integer> type. The type T of [IRequest<T>] (line 91) must be the type T of the method [Observable<T> getAlea] (line 89);
  • the [IRequest<T>] interface has only one method: getResponse. Its role is to provide the response of type T that the [Observable<T> getAlea] method must return;
  • line 94: it is the [WebClient] interface that provides this response. It is passed the two parameters received on line 89. For this reason, these must have the final attribute;

2.8.3.5. The [MainActivity]

  

The [MainActivity] activity is as follows:


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 {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
 
  // methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // continue the initializations begun by the parent class
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    // define fragments here
    return new AbstractFragment[]{new Vue1Fragment_()};
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    // define fragment titles here
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // tabbed browsing - define the view to be displayed
  }
 
  @Override
  protected int getFirstView() {
    return 0;
  }
 
  // interface IDao ------------------------------------------
  @Override
  public Observable<Response<Integer>> getAlea(int a, int b) {
    return dao.getAlea(a, b);
  }
 
}
  • Note that lines 15–61 are included by default in the [client-android-skel] project. You just need to customize them;
  • lines 40–44: the fragment array. There is only one here;
  • lines 47–51: no fragment titles are needed;
  • lines 53–56: no tabs here;
  • lines 58–61: the first view to display is view #0, that of [Vue1Fragment];
  • lines 64-67: implementation of the [IDao] interface. Here, there is nothing to do other than delegate the work to the [DAO] layer on line 23;

2.8.3.6. The state of the [Vue1Fragment] fragment

  

The [Vue1FragmentState] class will be as follows:


package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
import java.util.ArrayList;
import java.util.List;
 
public class Vue1FragmentState extends CoreState {
 
  // fragment status ------------------------
  // list of answers
  private List<String> reponses = new ArrayList<>();
  // condition view ------------------------
  // error msg on the number of random numbers requested
  private boolean txtErrorAleasVisible = false;
  // error msg on generation interval [a,b]
  private boolean txtErrorIntervalleVisible = false;
  // error msg on the URL of the web service
  private boolean txtMsgErreurUrlServiceWebVisible = false;
  // waiting time error msg
  private boolean textViewErreurDelayVisible = false;
  // whether or not the Execute button is visible
  private boolean btnExecuterVisible = true;
 
  // getters and setters
...
}

To determine what needed to be saved in the fragment, we rotated the device in various situations and observed what was lost upon restoration. We concluded that the information in lines 10–23 needed to be saved.

2.8.3.7. The fragment [View1Fragment]

  

Currently, the [Vue1Fragment] view contains various errors due to the fact that the parent class [AbstractFragment] from which it derives has changed. Rather than describing the changes to be made one by one, we will comment directly on the final version.

The fragment's skeleton is as follows:


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 {
 
...
}
  • Line 26: Note that every fragment must have a menu, even if it is empty. This is the case here.

2.8.3.7.1. Handling the click on the [Execute] button

@Click(R.id.btn_Executer)
  protected void doExecuter() {
    // check the data entered
    if (!isPageValid()) {
      return;
    }
    // delete previous answers
    reponses.clear();
    dataAdapterReponses.notifyDataSetChanged();
    // reset the response counter to 0
    nbReponses = 0;
    infoReponses.setText("Liste des réponses (0)");
    // activity initialization
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // prepare the random task
    beginWaiting(1);
    // we ask for the random numbers
    getAleasInBackground(nbAleas, a, b);
  }
 
  void getAleasInBackground(int nbAleas, int a, int b) {
    // create the process to be observed
    Observable<Response<Integer>> process = Observable.empty();
    for (int i = 0; i < nbAleas; i++) {
      process = process.mergeWith(mainActivity.getAlea(a, b));
    }
    // we ask for the random numbers
    executeInBackground(process, new Action1<Response<Integer>>() {
 
      @Override
      public void call(Response<Integer> response) {
        // we consume the answer
        consumeAleaResponse(response);
      }
    });
  }
 
  protected void consumeAleaResponse(Response<Integer> response) {
    // log
    if (isDebugEnabled) {
      try {
        Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // a + response
    nbReponses++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
    // we analyze the response
    // mistake?
    if (response.getStatus() != 0) {
      // display
      showAlert(response.getMessages());
      // cancellation
      doAnnuler();
      // back to Ui
      return;
    }
    // we add the information to the list of answers
    reponses.add(0, String.valueOf(response.getBody()));
    // refreshing the answers
    dataAdapterReponses.notifyDataSetChanged();
  }
 
  // cancellation ----------
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchronous tasks are cancelled
    cancelRunningTasks();
}
 
  private void beginWaiting(int nbRunningTasks) {
    // we set the hourglass
    beginRunningTasks(nbRunningTasks);
    // the [Cancel] button replaces the [Execute] button
    btnExecuter.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
  }
  • lines 4-6: First, we check that the entries are valid. Error messages may then appear;
  • lines 8-9: the list of responses is cleared. This change is reflected in the ListView that displays them;
  • lines 11-12: The number of responses received is reset to zero;
  • line 14: We set the URL for the random number service. This information will be passed to the [DAO] layer;
  • line 15: the timeout period before sending the request to the random number service is set. This information will be passed to the [DAO] layer;
  • line 17: we prepare to launch 1 asynchronous task (not N; we’ll see why);
  • lines 24–27: We combine the N asynchronous tasks into a single sequence of operations [merge];
  • lines 29–36: we ask the parent class [AbstractParent] to query the random number web service / JSON;
  • lines 29–36: the [executeInBackground] method expects two parameters:
    • line 29: the process to be observed and executed is the one calculated in the preceding lines;
    • lines 29–36: the [Action1] instance to be executed when the response from the asynchronous service is received. The type T of [Action1<T>] must be the type T of the result of the [getAlea] method, i.e., a [Response<Integer>] type;
  • line 34: when a response arrives (a random number), it is consumed in the method on line 39;
  • lines 49–50: we record and signal that a new response has been received;
  • lines 53–60: the type [Response<T>] has a [status] field that is an error code. If this code is non-zero, then the server encountered a problem;
  • line 55: an error message is displayed. The [showAlert] method belongs to the parent class;
  • line 57: the method in lines 68–75 is called. It will cancel any tasks that are still active (line 74);
  • line 62: the response is added to the list of responses, which is the data source for the ListView;
  • line 64: the ListView is refreshed;
  • lines 77–83: the [beginWaiting(int nbRunningTasks)] method prepares the view for waiting (lines 81–82) and notifies the parent class that [nbRunningTasks] tasks will soon be executed (line 79);

2.8.3.7.2. The fragment's lifecycle

The fragment's lifecycle is managed by the following methods:


  // local data
  private List<String> reponses;
  private ArrayAdapter<String> dataAdapterReponses;
  private int nbReponses = 0;
...
  // life cycle management ---------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // current view status
    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) {
    // 1st visit?
    if (previousState != null) {
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      reponses = state.getReponses();
    } else {
      reponses = new ArrayList<>();
    }
    // listView data source
    dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    // nre of responses
    nbReponses = reponses.size();
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // listview / adapter link
    listReponses.setAdapter(dataAdapterReponses);
    // 1st visit?
    if (previousState == null) {
      // hide error messages
      txtErrorAleas.setVisibility(View.INVISIBLE);
      txtErrorIntervalle.setVisibility(View.INVISIBLE);
      txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
      textViewErreurDelay.setVisibility(View.INVISIBLE);
      // buttons
      btnAnnuler.setVisibility(View.INVISIBLE);
      btnExecuter.setVisibility(View.VISIBLE);
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // previous view status
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    // show / hide error msg
    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);
    // buttons
    btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
    btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
    // no. of responses
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // the [Execute] button replaces the [Cancel] button
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);
 
}
  • lines 7–18: ensure the fragment is saved when the parent class requests it;
  • line 11: displays the error message regarding the timeout;
  • line 12: visibility of the error message regarding the number of random numbers requested;
  • line 13: visibility of the error message regarding the web service URL / JSON;
  • line 14: displays the error message regarding the [a,b] range for random number generation;
  • line 15: visibility of the [Run] button;
  • line 16: the list of received responses;
  • lines 20–23: must return the view ID. The fragment ID here is 0 since there is only one;
  • lines 25–38: initialization of the fragment’s fields, either on a first visit (previousState == null) or on a subsequent visit;
    • lines 29–30: if this is not the first visit, the [reponses] field is restored from the fragment’s previous state;
    • lines 31–33: if this is the first visit, then the [reponses] field is initialized with an empty list;
    • lines 34-37: using the [reponses] field, we can construct the data source for the fragment’s ListView (line 35) as well as the number of responses (line 37);
  • lines 40–55: executed to initialize the view associated with the fragment, either on a first visit (previousState == null) or on a subsequent visit;
    • line 43: the fragment’s ListView is bound to the data source that was just constructed in the [initFragment] method;
    • lines 45–54: if this is the first visit, the view is prepared for its first display;
  • lines 57–60: executed during inter-fragment navigation associated with a [SUBMIT] action. Here, there is only one fragment and therefore no inter-fragment navigation;
  • lines 63–76: executed during inter-fragment navigation associated with a [NAVIGATION] action or during a save/restore cycle due to device rotation or another reason. Here, only the latter case can occur. Remember that here, in all cases, [previousState] is always non-null;
  • line 65: the previous state is cast to the fragment state type;
  • lines 66–75: the contents of the previous state are used to restore the view;
  • lines 78–81: called when all previous updates have been completed. Here, there is nothing to do;
  • lines 83–89: executed when all asynchronous tasks are complete. Here, the [Cancel] button is hidden and replaced with the [Execute] button;

2.8.3.8. Tests

The reader is invited to perform the following tests:

  • create errors and run the device: the error messages must remain displayed;
  • Generate random numbers and run the device: the generated random numbers must remain displayed;
  • set a wait of several seconds and run the device during the wait: the tasks must have been canceled (this can be seen in the logs);

2.8.4. Example-22B

Here we revisit Example 22 to refactor it according to the [client-android-skel] project model. Recall that the [Example-22] project correctly handles the fragment save/restore cycle during rotation and that it served as the basis for the [client-android-skel] project.

We duplicate the [client-android-skel] project into [examples/Example-22B] and load the latter project:

  

Then we copy various elements from the [Example-22] project into the [Example-22B] project.

First, we copy elements from the [res] folder:

  • [layout/fragment_main.xml, layout/view1.xml, menu/menu_fragment.xml, menu/menu_main.xml, the [values] folder;
  

We will change the top margin of both views to 120 dp:

[view1.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"/>

Next, we copy the elements [View1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:

 

At this point, we can attempt a first compilation. A first type of error appears: incorrect imports because classes have changed packages. We correct these imports. A second type of error is due to the fact that the fragments do not implement all the methods of their parent class [AbstractFragment]. We correct this by pressing (Alt+Enter).

The remaining errors stem from differences between the old and new classes [AbstractFragment]. For now, we ignore them.

2.8.4.1. Project Customization

  

The [custom] folder contains architecture elements that can be customized by the developer.

The [IMainActivity] interface allows you to specify certain project characteristics:


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 0;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = true;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = false;
 
  // number of fragments
  int FRAGMENTS_COUNT = 5;
 
}
  • lines 23, 26, 29, 38: characteristics of the [DAO] layer. There are none here;
  • line 41: there are five fragments here;
  • line 32: fragment adjacency. This constant can take a value between [1,4] here. The reader is encouraged to vary this value to see if the application continues to function;
  • line 35: this is a tabbed application;

The [CoreState] class that stores the state of the fragments will be as follows:


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 {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • Line 12: We declare the fragment state class [PlaceHolderFragment]. The [Vue1Fragment] fragment itself has no state;

The [Session] class is as follows:


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // data to be shared between fragments themselves and between fragments and activities
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
  // don't forget the getters and setters required for serialization / deserialization jSON
 
  // number of fragments visited
  private int numVisit;
  // n° fragment type [PlaceholderFragment] displayed in second tab
  private int numFragment = -1;
 
  // getters and setters
...
}

This is the session for the [Example-22] project.

2.8.4.2. The [MainActivity]

  

The [MainActivity] activity is as follows:


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 {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // menu management-----------------------
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
...
  }
 
  private void showFragment(int i) {
...
  }
 
  // implementation of parent class methods ---------------------------------------------------
  ...
}

Here, the [MainActivity] class is larger than in the previous examples for two reasons:

  • there are tabs to manage;
  • there is a menu to manage;

2.8.4.2.1. Implementation of the parent class methods

// methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // continue the initializations begun by the parent class
    // session
    this.session = (Session) super.session;
    ...
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    // fragment no
    final String ARG_SECTION_NUMBER = "section_number";
    // initialization of fragment table
    AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
    int i;
    for (i = 0; i < fragments.length - 1; i++) {
      // create a fragment
      fragments[i] = new PlaceholderFragment_();
      // you can pass arguments to the
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, i + 1);
      fragments[i].setArguments(args);
    }
    // a fragment of +
    fragments[i] = new Vue1Fragment_();
    // result
    return fragments;
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    // no titles here
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
...
  }
 
  @Override
  protected int getFirstView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
  • lines 2–12: The [onCreateActivity] method is called by the parent class [AbstractActivity] when the activity is created for the first time or recreated during a save/restore cycle. When this method is called, the parent class has already restored the session;
  • line 10: a local reference to the session is retrieved. The type cast is necessary because the parent class’s session is of type [AbstractSession];
  • lines 19–38: the [getFragments] method must return to the parent class the array of fragments managed by the application. Here there are [FRAGMENTS_COUNT] fragments, a number defined in [IMainActivity]. The first [FRAGMENTS_COUNT-1] fragments are of type [PlaceHolderFragment] and the last one is of type [Vue1Fragment];
  • lines 41–45: the [getFragmentTitle] method must return the fragment titles when this information is useful. This is not the case here;
  • lines 47–50: this method is called by the parent class when the user clicks on a tab. We will return to this in the next section;
  • lines 52–55: returns the number of the first view to display when the application starts. Here, the [Vue1Fragment] fragment must be displayed first. The [getFirstView] method could advantageously be replaced by a constant in [IMainActivity];

2.8.4.2.2. Tab Management

Tabs are managed using the following methods:


@Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // continue the initializations begun by the parent class
    // session
    this.session = (Session) super.session;
    // 1st tab
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // 2nd tab ?
    int numFragment = session.getNumFragment();
    if (numFragment != -1) {
      TabLayout.Tab tab2 = tabLayout.newTab();
      tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
      tabLayout.addTab(tab2);
    }
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // fragment number to display
    int numFragment;
    switch (position) {
      case 0:
        // fragment no. [Vue1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // fragment no. [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // fragment display
    if (numFragment != mViewPager.getCurrentItem()) {
      navigateToView(numFragment, ISession.Action.SUBMIT);
    }
  }
}
  • lines 1–20: The [onCreateActivity] method is called by the parent class [AbstractActivity] when the activity is created for the first time or recreated during a save/restore cycle. When this method is called, the parent class has already restored the session;
  • line 9: a local reference to the session is retrieved. The type cast is necessary because the parent class’s session is of type [AbstractSession];
  • lines 11–13: the first tab is created;
  • lines 15–20: the second tab is created if a fragment ID is stored in the session (line 15). This ID is initially set to -1 when the activity is first constructed;
  • lines 23–39: this method is called by the parent class when the user clicks on a tab;
  • lines 28-31: if tab 0 is clicked, then [Vue1Fragment] must be displayed. We know that this is the first view that was displayed when the application started;
  • lines 32–35: if tab 1 is clicked, then the fragment whose number is stored in the session must be displayed;
  • lines 37–39: We navigate to the selected fragment. The associated action is [SUBMIT]. Could it have been [NAVIGATION]? In this document, we use [NAVIGATION] only when displaying the new fragment requires knowing only its previous state. Here, that is not the case, since the display of the fragment must change from its previous state to show one more visit;

2.8.4.2.3. Menu Management

The activity is associated with the following 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>

which displays the following:

  

The menu is managed by the following methods:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onOptionsItemSelected");
    }
    // processing menu options
    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 processed
    return true;
  }
 
  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // no navigation on software tab selection
      session.setNavigationOnTabSelectionNeeded(false);
      // we recreate the two tabs for a title font issue
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
      // the fragment number to be displayed is set in session
      session.setNumFragment(i);
      // select tab 2 with navigation
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • lines 16-31: handle a click on a [Fragmenti] menu option;
  • lines 37–50: display fragment #i (these are PlaceHolderFragment-type fragments) in tab #1 (the second tab);
  • lines 42-44: we decide to remove the existing tabs to create two new ones. This decision was made to work around the following issue: when we simply display the fragment in the existing tab 1 (without deleting it), curiously its title appears (font, size) different from that of tab 0’s title;
  • lines 43–44: the two tabs are created but not selected (last parameter set to false);
  • Line 40: The operations on lines 42–44 may trigger [select] operations on the tabs, which will call the [onTabSelected] handler. If no action is taken, this will result in navigation to a fragment. We prevent this by setting the [navigationOnTabSelectionNeeded] boolean to false in the session. This boolean is automatically reset to true by the [AbstractFragment] class when a fragment becomes visible;
  • line 46: we store the number of the fragment to be displayed in the session;
  • lines 48–50: Select tab #2 with navigation (line 48). This will trigger the [onTabSelected] procedure, which will:
    • display the fragment whose number was stored in the session;
    • store the number of the selected tab in the session;

2.8.4.3. The [Vue1Fragment] fragment

Here is the final version of the fragment:


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 {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
  // fragment life cycle -----------------------------------------------
  private void initFragment() {
    // nothing to do
  }
 
  // save fragment status
  @Override
  public CoreState saveFragment() {
    // view status - nothing to save
    return new CoreState();
  }
 
  @Override
  protected int getNumView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // nothing to do
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // 1st visit?
    if (previousState == null) {
      // the visit number is displayed
      showNumVisit();
    }
 
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // the visit number is displayed
    showNumVisit();
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
 
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
 
  }
 
  // private methods -------------------------------------
  // display visit no
  private void showNumVisit() {
    // increment visit no
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // the visit number is displayed
    Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}

The class is almost empty.

  • lines 35-39: called by the parent class when the fragment needs to save its state. The [Vue1Fragment] fragment has no state to save. We simply return an instance of the base class [CoreState] (reminder: we must not return null);
  • lines 41-44: must return the fragment ID. By design, the [Vue1Fragment] fragment has the ID [FRAGMENTS_COUNT-1];
  • lines 51-59: called by the parent class when the fragment is constructed for the first time (previousState == null) or on subsequent visits (previousState != null);
    • lines 54-57: if this is the first visit, increment the visit count and display it (lines 85-92);
  • lines 61-65: called when the fragment is about to be displayed in association with a [SUBMIT] action. The visit count is incremented and displayed. Here, it is not possible for the visit count to be incremented twice during the lifecycle. In fact, the first visit to the fragment [Vue1Fragment] occurs at application startup when the action is set to [NONE] by design in the session. This ensures that the [updateOnSubmit] method will not be called. After that, it will never be the first visit again, and the [initView] method will do nothing;
  • lines 68–71: called during a save/restore cycle. Since the fragment has no state, there is nothing to restore here;
  • lines 73–76: called when all previous updates have been completed. Here, there is nothing left to do;
  • lines 78–81: called when all launched asynchronous tasks have completed. Here, there are no asynchronous tasks;

2.8.4.4. The [PlaceHolderFragmentState] state

The state of the [PlaceHolderFragment] fragment will be as follows:


package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class PlaceHolderFragmentState extends CoreState {
  // text
  private String text;
 
  // manufacturers
  public PlaceHolderFragmentState() {
 
  }
 
  public PlaceHolderFragmentState(String text) {
    super();
    this.text = text;
  }
 
  // getters and setters
 ...
}
  • When we need to save the fragment's state, we'll save the text it was displaying (line 7);

2.8.4.5. The [PlaceHolderFragment] fragment

The [PlaceHolderFragment] fragment will be as follows:


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 {
 
  // visual interface components
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  @ViewById(R.id.textView1)
  protected TextView textView1;
 
  // data
  private String text;
 
  // fragment no
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // implementation of parent class methods ----------------------------
  @Override
  public CoreState saveFragment() {
    // save fragment state
    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) {
    // original text
    text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
  }
 
  @Override
  protected void initView(CoreState previousState) {
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // update the text displayed
    // increment visit no
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
    // log
    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) {
    // restore displayed text
    PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
    textViewInfo.setText(state.getText());
 
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
 
  }
 
}
  • lines 30–36: when the parent class asks the fragment to save its state, the text displayed by the fragment is saved (line 34);
  • lines 38–41: return the fragment’s ID. This depends on the section ID passed as an argument when the fragment was created;
  • lines 43–47: called during the fragment’s first construction (previousState == null) or during subsequent constructions (previousState != null);
    • line 46: here, the previous state is not used. The initial text [text] (line 24) displayed on the first visit is recalculated each time. This is debatable. We could have chosen to also include this information in the fragment’s state;
  • lines 49–51: called during the first rendering of the view associated with the fragment (previousState == null) or during subsequent renderings (previousState != null). There is nothing to do;
  • lines 53–56: called when the fragment is about to be displayed in association with a [SUBMIT] action. This is always the case except during the save/restore cycle, where the action is [RESTORE]. We therefore increment the visit number and display it;
  • lines 68–74: called during a save/restore cycle. We restore the text that was saved in the fragment’s state;
  • lines 76–79: called when all previous updates have been completed. Here, there is nothing further to do;
  • lines 82-83: called when all launched asynchronous tasks are complete. Here, there are no asynchronous tasks;

2.8.4.6. Tests

The reader is invited to test the application by rotating the device to verify that the displayed fragment does not lose its state. We will also examine the logs.

2.9. Conclusion

At the end of this chapter, we have a sample project [client-android-skel] for an Android client communicating with a web service / JSON with the following features:

  • Asynchronous communication with the web/JSON server is handled using the RxJava library;
  • the fragment’s lifecycle (update, save, restore) is managed by its parent class [AbstractFragment], which calls specific methods of its child classes at precise moments. The child fragment thus does not need to concern itself with the lifecycle stages but only with implementing certain methods required by its parent class;
  • the activity's lifecycle (save / restore) is managed by an abstract class [AbstractActivity], which also requires the child activity to implement certain methods;
  • The [AbstractActivity] class can handle an application with or without tabs, with or without a loading image, and with or without basic authentication against the web server / JSON. The presence or absence of these elements is determined by configuration;

We will now present a case study that is more complex than the previous examples. The new application will be based on the [client-android-skel] template project.