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 must 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'
}
}
// packaging options required to generate the 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">[Name your app]</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
![]() | ![]() |

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 {
// controls swiping
private boolean isSwipeEnabled;
// controls scrolling
private boolean isScrollingEnabled;
// constructors
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// methods to override to handle swiping
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// Is swiping allowed?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Is swiping allowed?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// scroll 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 the subclasses of [CoreState] here
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// whether the fragment has been visited
protected boolean hasBeenVisited = false;
// state of the fragment's menu (if any)
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:

package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// access to the session
ISession getSession();
// change 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 wait time for server response
int TIMEOUT = 1000;
// wait time before executing the client 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;
// loading icon
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 setWebServiceJsonUrl(String url);
// User
void setUser(String user, String password);
// Client timeout
void setTimeout(int timeout);
// Basic authentication
void setBasicAuthentication(boolean isBasicAuthenticationNeeded);
// 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 the last view displayed
int getPreviousView();
void setPreviousView(int numView);
// Last state of a view
CoreState getCoreState(int numView);
void setCoreState(int numView, CoreState coreState);
// current action
enum Action {
SUBMIT, NAVIGATION, RESTORE, NONE
}
Action getAction();
void setAction(Action action);
// States of all views -
// Not used by the code but required for JSON serialization/deserialization
CoreState[] getCoreStates();
void setCoreStates(CoreState[] coreStates);
// number of the last selected tab
int getPreviousTab();
void setPreviousTab(int position);
// navigation on tab selection
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 ID
private int previousView;
// View state
private CoreState[] coreStates = new CoreState[0];
// current action
private Action action = Action.NONE;
// previously selected tab
private int previousTab;
// navigate to selected tab
@JsonIgnore
private boolean navigationOnTabSelectionNeeded = true;
// constructor
public AbstractSession() {
// initialize the fragment state array
coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
for (int i = 0; i < coreStates.length; i++) {
coreStates[i] = new CoreState();
}
}
// ISession interface ---------------------------------------------------------
@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 the activity
// Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
// Don't forget the getters and setters required for JSON serialization/deserialization
}
- 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 {
// [DAO] layer
private IDao dao;
// the session
protected Session session;
// fragment container
protected MyPager mViewPager;
// the toolbar
private Toolbar toolbar;
// the loading image
private ProgressBar loadingPanel;
// tab bar
protected TabLayout tabLayout;
// the fragment or section manager
private FragmentPagerAdapter mSectionsPagerAdapter;
// class name
protected String className;
// JSON mapper
private ObjectMapper jsonMapper;
// constructor
public AbstractActivity() {
// class name
className = getClass().getSimpleName();
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "constructor");
}
// jsonMapper
jsonMapper = new ObjectMapper();
}
// IMainActivity implementation --------------------------------------------------------------------
...
// lifecycle - saving/restoring the activity ------------------------------------
...
// handling the splash screen ---------------------------------
...
// IDao interface -----------------------------------------------------
...
// the fragment manager --------------------------------
...
// child 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:
// IMainActivity implementation --------------------------------------------------------------------
@Override
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position, ISession.Action action) {
if (IS_DEBUG_ENABLED) {
Log.d(className, String.format("navigating to view %s on action %s", position, action));
}
// display new fragment
mViewPager.setCurrentItem(position);
// Record the current action during this view change
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):
// Activity save/restore management ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// parent
super.onSaveInstanceState(outState);
// Save session as a 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");
}
// Anything to restore?
if (savedInstanceState != null) {
// restore session
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");
}
...
// [DAO] layer
dao = getDao();
if (dao != null) {
// [DAO] layer configuration
setDebugMode(IS_DEBUG_ENABLED);
setTimeout(TIMEOUT);
setDelay(DELAY);
setBasicAuthentication(IS_BASIC_AUTHENTIFICATION_NEEDED);
}
...
// child 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);
// loading icon?
if (IS_WAITING_ICON_NEEDED) {
// add the loading image
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding loadingPanel");
}
// Create ProgressBar
loadingPanel = new ProgressBar(this);
loadingPanel.setVisibility(View.INVISIBLE);
// Add the ProgressBar to the 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 navigation on selection until a fragment is displayed
session.setNavigationOnTabSelectionNeeded(false);
// create tab bar
tabLayout = new CustomTabLayout(this);
tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
// Add the tab bar to the app bar
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
appBarLayout.addView(tabLayout);
// Event handler for the tab bar
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 #%s, action=%s, tabCount=%s, isNavigationOnTabSelectionNeeded=%s",
tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
}
if (session.isNavigationOnTabSelectionNeeded()) {
// tab position
int position = tab.getPosition();
// memory
session.setPreviousTab(position);
// display associated fragment?
navigateOnTabSelected(position);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
...
// child 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:
- The code for the [CustomTabLayout] class was found at the URL [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout];
- The fonts were found at the URL [https://www.fontsquirrel.com/fonts/roboto];
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");
}
...
// instantiate the fragment manager
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e., fragment #i in the fragment container is fragment #i returned by the fragment manager
mViewPager = (MyPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// Disable swiping between fragments
mViewPager.setSwipeEnabled(false);
// fragment adjacency
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// display the first view
if (session.getAction() == ISession.Action.NONE) {
navigateToView(getFirstView(), ISession.Action.NONE);
}
// Handle over to the child activity
onCreateActivity();
}
...
// child 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:
// managing the waiting image ---------------------------------
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 {
// [DAO] layer
private IDao dao;
...
// IDao interface -----------------------------------------------------
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setUser(String user, String password) {
dao.setUser(user, password);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
@Override
public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
dao.setBasicAuthentication(isBasicAuthenticationNeeded);
}
@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;
// constructor
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
// fragments of the child class
fragments = getFragments();
}
// must return the fragment at position
@Override
public AbstractFragment getItem(int position) {
// return the fragment
return fragments[position];
}
// Returns the number of fragments to manage
@Override
public int getCount() {
return fragments.length;
}
// returns the title of the fragment at position
@Override
public CharSequence getPageTitle(int position) {
return getFragmentTitle(position);
}
}
// child 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 restoring, then restore the 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:
// child 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 {
// [DAO] layer
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// parent class methods -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
// todo: continue the initializations started by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// todo: define the 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: tab navigation - define the view to display
}
@Override
protected int getFirstView() {
// todo: tab navigation - set the first view to display
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

![]() |
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 setWebServiceJsonUrl(String url);
// User
void setUser(String user, String password);
// Client timeout
void setTimeout(int timeout);
// Basic authentication
void setBasicAuthentication(boolean isBasicAuthenticationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before sending a 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 the URLs to be accessed here
}
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 password;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// HTTP headers of the intercepted HTTP request
HttpHeaders headers = request.getHeaders();
// Basic HTTP authentication header
HttpAuthentication auth = new HttpBasicAuthentication(user, password);
// Add to HTTP headers
headers.setAuthorization(auth);
// continue the HTTP request lifecycle
return execution.execute(request, body);
}
// authentication elements
public void setUser(String user, String password) {
this.user = user;
this.password = password;
}
}
This class generates the following HTTP authentication header:
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 {
// JSON mapper
private ObjectMapper mapper = new ObjectMapper();
// debug mode
protected boolean isDebugEnabled;
// class name
protected String className;
// delay before executing the request
private int delay;
// constructor
public AbstractDao() {
// class name
className = getClass().getName();
Log.d("AbstractDao", String.format("constructor, thread=%s", Thread.currentThread().getName()));
}
// protected methods ----------------------------------------------------------
// 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 - waiting for a single response
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// service execution
try {
// wait?
if (delay > 0) {
Thread.sleep(delay);
}
// execute the synchronous request
T response = request.getResponse();
// log
if (isDebugEnabled) {
String log;
if (response is of type String) {
log = (String) response;
} else {
log = mapper.writeValueAsString(response);
}
Log.d(className, String.format("response=%s on thread [%s]", log, Thread.currentThread().getName()));
}
// send the response to the observer
subscriber.onNext(response);
// signal the end of the observable
subscriber.onCompleted();
} catch (InterruptedException | JsonProcessingException | RuntimeException e) {
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("Thread [%s], Server communication exception: %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
} catch (JsonProcessingException e1) {
Log.d(className, String.format("Unexpected JSON error"));
}
}
// throw an exception
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 client
@RestService
protected WebClient webClient;
// security
@Bean
protected MyAuthInterceptor authInterceptor;
// the RestTemplate
private RestTemplate restTemplate;
// RestTemplate factory
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// create the RestTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// Set the JSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// Set the RestTemplate for the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// Set the web service URL
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String password) {
// Register the user in the interceptor
authInterceptor.setUser(user, password);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// configuration factory
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentication thread=%s, isBasicAuthenticationNeeded=%s", Thread.currentThread().getName(), isBasicAuthenticationNeeded));
}
// authentication interceptor?
if (isBasicAuthenticationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// private methods -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// todo: implement 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 item ID
private int menuItemId;
// visibility of the option
private boolean isVisible;
// constructors
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 messages from an exception - version 1
static public List<String> getMessagesFromException(Throwable ex) {
// create a list containing the error messages from the exception stack
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// List of messages for an exception - version 2
static public String getMessageForAlert(Throwable th) {
// build the text to display
StringBuilder text = new StringBuilder();
List<String> messages = getMessagesFromException(th);
int n = messages.size();
for (String message : messages) {
text.append(String.format("%s : %s\n", n, message));
n--;
}
// result
return text.toString();
}
// list of messages for an exception - version 3
static public String getMessageForAlert(List<String> messages) {
// build the text to display
StringBuilder text = new StringBuilder();
int n = messages.size();
for (String message : messages) {
text.append(String.format("%s : %s\n", n, message));
n--;
}
// result
return text.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 {
// private data ------------------------------------------------------------
// subscriptions to observables
private List<Subscription> subscriptions = new ArrayList<>();
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates = new MenuItemState[0];
// fragment lifecycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment state
private CoreState previousState;
// JSON mapper
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment lifecycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
// Asynchronous tasks
private boolean runningTasksHaveBeenCanceled;
// data accessible to child 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 ------------------------------------------
...
// Queue management -------------------------------------------------------------
...
// asynchronous operation management --------------------------------------------------------------------
...
// exception handling -------------------------------------------------------------------
....
// fragment lifecycle management --------------------------------------------------------
...
// child classes -----------------------------------------------------
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 lifecycle
private boolean fragmentHasToBeInitialized = false;
...
// constructor ----------------------
public AbstractFragment() {
// init
className = getClass().getSimpleName();
fragmentHasToBeInitialized = true;
// log
if (isDebugEnabled) {
Log.d(className, "constructor");
}
}
- 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) {
// iterate through all menu items
for (int i = 0; i < menu.size(); i++) {
// item #i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// if item #i is a submenu, then we start over
if (menuItem.hasSubMenu()) {
// recursion
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
private void getMenuOptionsStates(Menu menu) {
// result
if (isDebugEnabled) {
Log.d(className, "getMenuOptionsStates(Menu)");
}
// retrieve the IDs of the menu options
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// transfer the menu options to an array
menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
for (int i = 0; i < menuOptionsStates.length; i++) {
// option ID
int id = menuOptionsIds.get(i);
// option state
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// result
if (isDebugEnabled) {
Log.d(className, String.format("Number of menu options=%s", menuOptionsStates.length));
}
}
// menu option states
private MenuItemState[] getMenuOptionsStates() {
MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
for (int i = 0; i < menuOptionsStates.length; i++) {
// state
MenuItemState state = this.menuOptionsStates[i];
// menu ID
int id = state.getMenuItemId();
// initialize state
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> subscriptions = new ArrayList<>();
// asynchronous tasks
protected int numberOfRunningTasks;
protected boolean tasksInBackgroundHaveBeenCanceled;
...
// Handling the wait for the completion of an asynchronous operation -------------------------------------
protected void beginRunningTasks(int numberOfRunningTasks) {
// note the number of tasks that will be executed
this.numberOfRunningTasks = numberOfRunningTasks;
// display the loading image
mainActivity.beginWaiting();
// clear the list of subscriptions
subscriptions.clear();
// No cancellations yet
runningTasksHaveBeenCanceled = false;
}
protected void cancelWaitingTasks() {
// hide the loading 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
// exception handling -------------------------------------------------------------------
// display exception alert
protected void showAlert(Throwable th) {
// display messages from the exception stack of Throwable th
new android.app.AlertDialog.Builder(activity).setTitle("Errors have occurred").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Close", null).show();
}
// display list of messages
protected void showAlert(List<String> messages) {
// Display the list of messages
new android.app.AlertDialog.Builder(activity).setTitle("Errors have occurred").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Close", 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> subscriptions = new ArrayList<>();
// asynchronous tasks
private boolean runningTasksHaveBeenCanceled;
protected int numberOfRunningTasks;
...
// executing an asynchronous task with RxAndroid
protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// process: the observable to execute/observe
// consumeResult: the method that processes the response
//
// new subscriptions are created only if there has been no cancellation
if (!runningTasksHaveBeenCanceled) {
// Execute on the I/O thread and observe on the UI thread
process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
// execute the observable
try {
subscriptions.add(process.subscribe(
// consume result
consumeResult,
// consume 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 threw an exception
// or an exception 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 task to wait for
numberOfRunningTasks--;
// Done?
if (numberOfRunningTasks == 0) {
// end wait
cancelWaitingTasks();
// signal the end of tasks to the child class
notifyEndOfTasks(false);
}
}
...
// child classes -----------------------------------------------------
...
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 threw an exception
// or an exception occurred during the execution of an asynchronous operation
private void consumeThrowable(Throwable th) {
// th: the exception to be handled
//
// log
if (isDebugEnabled) {
Log.d(className, "Exception received");
}
// cancel tasks that have already been started
cancelRunningTasks();
// display error messages
showAlert(th);
}
// cancel tasks
protected void cancelRunningTasks() {
// log
if (isDebugEnabled) {
Log.d(className, "Canceling running tasks");
}
// cancel all registered asynchronous tasks
for (Subscription subscription : subscriptions) {
subscription.unsubscribe();
}
// note the cancellation
runningTasksHaveBeenCanceled = true;
numberOfRunningTasks = 0;
// end of wait
cancelWaitingTasks();
// Notify the child fragment of the task cancellation
notifyEndOfTasks(true);
}
...
// child classes -----------------------------------------------------
...
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
// Lifecycle --------------------------------------------------------
@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 lifecycle
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// save?
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 the fragment only if it is visible
if (isVisibleToUser) {
// maybe the save has already been done
if (!saveFragmentDone) {
saveState();
}
// restore in any case
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:
- when it transitions from the visible state to the hidden state;
- when the device rotates;
The private method [saveState] is as follows:
...
private void saveState() {
// tasks to cancel?
if (numberOfRunningTasks != 0) {
// cancel the tasks
cancelRunningTasks();
}
// Save the fragment's state
CoreState currentState = saveFragment();
// the fragment has been visited
currentState.setHasBeenVisited(true);
// save menu state
currentState.setMenuOptionsState(getMenuOptionsStates());
// set session
session.setCoreState(getNumView(), currentState);
// Save complete
saveFragmentDone = true;
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
...
// child classes -----------------------------------------------------
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 lifecycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// JSON mapper
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment lifecycle
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 the menu options if this hasn't already been 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 has any meaning)
previousState = session.getCoreState(getNumView());
// Update the child fragment in several steps
// step 1 - is this the first visit?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
...
} else {
// this is not the first visit
// step 2: should the fragment be initialized?
...
// Step 3: Should the view be initialized?
...
}
// Step 4: A submit, a navigation, a restore?
...
// Step 5: Terminal updates ----------------------
...
}
...
// child classes -----------------------------------------------------
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 lifecycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// JSON mapper
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment lifecycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited has any meaning)
previousState = session.getCoreState(getNumView());
// Update the child fragment in several steps
// step 1 - is this the first visit?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
// Initialize fragment and view
initFragment(null);
initView(null);
// reset previousState for later
previousState = null;
} else {
// this is not the first 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 lifecycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// JSON mapper
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment lifecycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited has any meaning)
previousState = session.getCoreState(getNumView());
// Update the child fragment in several steps
// step 1 - is this the first visit?
if (!previousState.getHasBeenVisited()) {
...
} else {
// This is not the first visit
// Step 2: Does the fragment need to be initialized?
if (fragmentHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initializing fragment");
}
// child fragment
initFragment(previousState);
}
// Step 3: Does the view need to be initialized?
if (viewHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "view initialization");
}
// child 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 lifecycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// JSON mapper
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment lifecycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited has any meaning)
previousState = session.getCoreState(getNumView());
// Update the child fragment in several steps
...
// step 4: a submit, a navigation, a restore?
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
Log.d(className, String.format("previous state=%s", jsonMapper.writeValueAsString(previousState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// current action
ISession.Action action = session.getAction();
switch (action) {
case SUBMIT:
if (isDebugEnabled) {
Log.d(className, "updateOnSubmit");
}
// child fragment
updateOnSubmit(previousState);
break;
case NAVIGATION:
if (isDebugEnabled) {
Log.d(className, "updateForNavigation");
}
if (previousState != null) {
// restore menu
setMenuOptionsStates(previousState.getMenuOptionsState());
// child fragment
updateOnRestore(previousState);
} else {
// this is a first visit - nothing to do
}
break;
case RESTORE:
// restore
if (isDebugEnabled) {
Log.d(className, "updateOnRestore");
}
// restore menu (previousState cannot be null)
setMenuOptionsStates(previousState.getMenuOptionsState());
// child 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 lifecycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// JSON mapper
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment lifecycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// Step 5: Terminal updates ----------------------
// we have changed views
session.setPreviousView(getNumView());
// no more actions in progress
session.setAction(ISession.Action.NONE);
// When we leave this fragment, it must be saved
saveFragmentDone = false;
// as long as the fragment hasn't been rebuilt, it doesn't need to be initialized
fragmentHasToBeInitialized = false;
// as long as the view is not rebuilt, it does not need to be initialized
viewHasToBeInitialized = false;
// We return to normal tab selection behavior
session.setNavigationOnTabSelectionNeeded(true);
// Notify the fragment 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 the 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) {
// managing the wait for the completion of a series of asynchronous tasks
//-- protected void beginRunningTasks(int numberOfRunningTasks) {
//-- protected void cancelWaitingTasks() {
// executing an asynchronous task with RxAndroid
//-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// Cancel tasks
//-- protected void cancelRunningTasks() {
// display an alert on exception
//-- protected void showAlert(Throwable th) {
// display list of messages
//-- protected void showAlert(List<String> messages) {
// methods required by the parent class -------------------------------------------------------
@Override
public CoreState saveFragment() {
// the fragment must be saved
DummyFragmentState state = new DummyFragmentState();
// ...
return state;
// if there is nothing to save, use [return new CoreState();] and remove the [DummyFragmentState] class
}
@Override
protected int getNumView() {
// Return the fragment number in the array of fragments managed by the activity (see MainActivity)
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// The fragment becomes visible and has been constructed in this step or a previous step
// This occurs when the application starts and every time the Android device rotates
// is necessarily followed by the execution of [initView]
// the fields of the fragment that has been reconstructed must be initialized
// previousState is the fragment's last saved state—is null if this is the first time the fragment is visited
}
@Override
protected void initView(CoreState previousState) {
// The fragment becomes visible and the associated view has been reconstructed in this step or a previous step
// this occurs every time [initFragment] is executed and every time the fragment leaves the vicinity of the displayed fragment
// The components of the view that has been reconstructed must be initialized
// previousState is the fragment's last saved state—is null if this is the first time the fragment is visited
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// is executed after [initFragment, initView] if these methods are executed
// the view will be displayed after a SUBMIT operation
// You generally need to initialize the fragment and the associated view from the session
// previousState is the last saved state of the fragment—is null if this is the first visit to the fragment
// there is nothing to do if the fragment cannot be reached via a SUBMIT operation
// if the fragment can be reached via SUBMIT operations from different fragments, the previous view can be obtained via [session.getPreviousView]
// if the fragment can be reached via multiple SUBMIT operations from the same fragment, then a flag must be set in the session to distinguish between the different types of SUBMIT operations originating from that fragment
}
@Override
protected void updateOnRestore(CoreState previousState) {
// is executed after [initFragment, initView] if these methods are executed
// the view will be displayed after a RESTORE or NAVIGATION operation
// previousState is the last saved state of the fragment—never null
// the view must be restored to its previous state
}
@Override
protected void notifyEndOfUpdates() {
// occurs after the [updateOnSubmit, updateOnRestore] methods
// At this point, the view has been constructed and initialized
// There is often nothing to do here, but you can also factor in actions that need to be performed regardless of how you arrive at this view
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// called when the asynchronous tasks launched by the fragment have either completed or been canceled
// These two cases can be distinguished using the runningTasksHaveBeenCanceled parameter
// generally, the view must be reset to a state different from the one it was in while waiting for responses from the 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 {
// state of the [DummyFragment] fragment
// include only fields serializable as JSON
// Add the @JsonIgnore annotation to the others, though it's unclear what purpose they might serve
// 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:

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

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/textViewFormTitle"
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/view1_title"
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]:

Generate the missing methods (Alt-Enter).
Another compilation error reported is as follows:

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 {
// access to the session
ISession getSession();
// change view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// application constants -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum wait time for the server response
int TIMEOUT = 1000;
// timeout before executing the client 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;
// loading icon
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 to JSON must have the @JsonIgnore annotation
}
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 {
// whether the fragment has been visited
protected boolean hasBeenVisited = false;
// state of the fragment's menu (if any)
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 {
// [DAO] layer
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// parent class methods -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
// todo: continue the initializations started by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// todo: define the 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) {
// To-do: tab navigation - define the view to display when tab #[position] is selected
}
@Override
protected int getFirstView() {
// todo: define the number of the first view (fragment) to display
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 {
// [DAO] layer
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// parent class methods -----------------------
@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 {
// the values of the dropdown list
private List<String> list;
// the selected item in the dropdown list
private int listSelectedPosition;
// the message displayed in the dialog box
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):
// dropdown list
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// Check the first button
radioButton1.setChecked(true);
// the calendar
datePicker1.setCalendarViewShown(false);
// the 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 dropdown list
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
}
...
protected void updateFragment() {
// initialize the dropdown 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) {
// First visit?
if (previousState == null) {
// Create the values for the dropdown list
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
} else {
// restore the values from the dropdown list
Vue1FragmentState state = (Vue1FragmentState) previousState;
list = state.getList();
// and the dialog message
message = state.getMessage();
}
// initialize the dropdown 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);
// the 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 the drop-down list adapter
dropDownList.setAdapter(dataAdapter);
// First 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()));
// selected item in dropdown list
Vue1FragmentState state = (Vue1FragmentState) previousState;
dropDownList.setSelection(state.getListSelectedPosition());
// Is the dialog visible?
if (message != null) {
// 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:
// the dialog message
private String message;
...
@Click(R.id.formulaireButtonValider)
protected void doValidate() {
// list of messages to display
List<String> messages = new ArrayList<>();
...
// display
doDisplay(messages);
}
private void display(final List<String> messages) {
// construct the text to be displayed
StringBuilder text = new StringBuilder();
for (String message : messages) {
text.append(String.format("%s\n", message));
}
// store the message
message = text.toString();
// display it
showMessage();
}
private void showMessage() {
// display it
new AlertDialog.Builder(activity).setTitle("Entered values").setMessage(message).setNeutralButton("Close", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// reset the message
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:

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 {
// access to the session
ISession getSession();
// change view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// application constants -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum wait time for server response
int TIMEOUT = 1000;
// wait time before executing the client 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;
// loading icon
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 the subclasses of [CoreState] here
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// whether the fragment has been visited
protected boolean hasBeenVisited = false;
// state of the fragment's menu (if any)
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 to JSON must have the @JsonIgnore annotation
}
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 setWebServiceJsonUrl(String url);
// User
void setUser(String user, String password);
// Client timeout
void setTimeout(int timeout);
// Basic authentication
void setBasicAuthentication(boolean isBasicAuthenticationNeeded);
// 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 client
@RestService
protected WebClient webClient;
// security
@Bean
protected MyAuthInterceptor authInterceptor;
// the RestTemplate
private RestTemplate restTemplate;
// RestTemplate factory
private SimpleClientHttpRequestFactory factory;
// timeout
private int timeout;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// create the RestTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// Set the JSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// Set the RestTemplate for the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// Set the web service URL
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String password) {
// Register the user in the interceptor
authInterceptor.setUser(user, password);
}
@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 setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentication thread=%s, isBasicAuthenticationNeeded=%s", Thread.currentThread().getName(), isBasicAuthenticationNeeded));
}
// authentication interceptor?
if (isBasicAuthenticationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// private methods -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// weather service ---------------------------------------------------------
@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 {
// [DAO] layer
@Bean(Dao.class)
protected IDao dao;
// Parent class methods -----------------------
@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;
}
// IDao interface ---------------------------------------------------------------------
@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.util.Log;
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="Build your visual interface"
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/actionWeather"
android:title="@string/actionMeteo"/>
<item
android:id="@+id/actionCancel"
android:title="@string/actionCancel"/>
<item
android:id="@+id/actionFinish"
android:title="@string/actionFinish"/>
</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 countOfReceivedResponses;
// event handling ---------------------------------------------------------------------------------------
// cities for which we want the weather
final String[] paysDeLoire = new String[]{"Angers", "Le Mans", "Nantes", "Laval", "La Roche-sur-Yon"};
@OptionsItem(R.id.actionMeteo)
protected void getWeather() {
// its country
String country = "fr";
// Get an API key by creating an account [https://home.openweathermap.org/users/sign_up]
String APPID = "xyz";
// Web service URL / JSON
mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
// Start waiting for [paysDeLoire.length] asynchronous tasks
beginWaiting(paysDeLoire.length);
// Number of responses received
nbResponsesReceived = 0;
// make asynchronous calls in parallel
for (String city : paysDeLoire) {
// weather
executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
@Override
public void call(String response) {
// process the response
consumeResponse(response);
// a response received
nbReceivedResponses++;
}
});
}
}
// process server response
private void consumeResponse(String response) {
// log
Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
}
// start waiting
protected void beginWaiting(int numberOfRunningTasks) {
// log
if (isDebugEnabled) {
Log.d(className, "beginWaiting");
}
// parent
beginRunningTasks(numberOfRunningTasks);
// display the [Cancel] option
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{
new MenuItemState(R.id.menuActions, true),
new MenuItemState(R.id.actionCancel, true)});
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu
initMenu();
// display results
String message;
switch (numberOfResponsesReceived) {
case 0:
message = "No responses were received";
break;
case 1:
message = "One response was received. Check your logs...";
break;
default:
message = String.format("%s responses were received. Check your 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.actionCancel, false)});
}
// lifecycle 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 doAnnul() {
if (isDebugEnabled) {
Log.d(className, "Cancelation requested");
}
// cancel asynchronous tasks
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 doTerminate() {
// shut everything down
System.exit(0);
}
The fragment's lifecycle is managed by the following methods:
// lifecycle 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) {
// First 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:
// cities for which we want the weather
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:


The logs are as follows:
07-23 13:24:30.899 2642-2642/client.android D/MainActivity_: constructor
07-23 13:24:30.945 2642-2642/client.android D/AbstractDao: constructor, 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_: setBasicAuthentication thread=main, isBasicAuthenticationNeeded=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_: constructor
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: navigating to view 0 on 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_: Number of menu options=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_: previousState=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} on thread [RxIoScheduler-4]
07-23 13:24:45.035 2642-2963/client.android D/client.android.dao.service.Dao_: response={} on thread [RxIoScheduler-6]
07-23 13:24:45.035 2642-2959/client.android D/client.android.dao.service.Dao_: response={} on thread [RxIoScheduler-2]
07-23 13:24:45.035 2642-2962/client.android D/client.android.dao.service.Dao_: response={} on thread [RxIoScheduler-5]
07-23 13:24:45.036 2642-2960/client.android D/client.android.dao.service.Dao_: response={} on 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 = "";

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], Server communication exception: [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], Server communication exception: [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], Server communication exception: [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], Server communication exception: [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception received
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Canceling launched tasks
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], Server communication exception: [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_: Cancellation requested
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Canceling launched tasks
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], Server communication exception: [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Server communication exception: [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Server communication exception: [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Server communication exception: [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Server communication exception: [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:

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

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 {
// access to the session
ISession getSession();
// change view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// application constants -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// Maximum wait time for the server response
int TIMEOUT = 1000;
// wait time before executing the client 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;
// loading icon
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 {
// whether the fragment has been visited
protected boolean hasBeenVisited = false;
// state of the fragment's menu (if any)
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 to JSON must have the @JsonIgnore annotation
}
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;
// response body
private T body;
// constructors
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> getRandom(@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 setWebServiceJsonUrl(String url);
// User
void setUser(String user, String password);
// Client timeout
void setTimeout(int timeout);
// Basic authentication
void setBasicAuthentication(boolean isBasicAuthenticationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// random number service
Observable<Response<Integer>> getRandom(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 client
@RestService
protected WebClient webClient;
// security
@Bean
protected MyAuthInterceptor authInterceptor;
// the RestTemplate
private RestTemplate restTemplate;
// RestTemplate factory
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// create the RestTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// Set the JSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// Set the RestTemplate for the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// Set the web service URL
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String password) {
// Register the user in the interceptor
authInterceptor.setUser(user, password);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// configuration factory
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentication thread=%s, isBasicAuthenticationNeeded=%s", Thread.currentThread().getName(), isBasicAuthenticationNeeded));
}
// authentication interceptor?
if (isBasicAuthenticationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// private methods -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// random number service
@Override
public Observable<Response<Integer>> getRandom(final int a, final int b) {
// web client execution
return getResponse(new IRequest<Response<Integer>>() {
@Override
public Response<Integer> getResponse() {
return webClient.getRandom(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 {
// [DAO] layer
@Bean(Dao.class)
protected IDao dao;
// Parent class methods -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// Continue the initializations started 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) {
// tab navigation - set the view to display
}
@Override
protected int getFirstView() {
return 0;
}
// IDao interface ------------------------------------------
@Override
public Observable<Response<Integer>> getRandom(int a, int b) {
return dao.getRandom(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 state ------------------------
// list of responses
private List<String> answers = new ArrayList<>();
// view status ------------------------
// error message regarding the number of random numbers requested
private boolean txtErrorRandomVisible = false;
// error message regarding the [a,b] generation interval
private boolean txtErrorIntervalVisible = false;
// error message regarding the web service URL
private boolean txtWebServiceURLErrorVisible = false;
// error message regarding the wait time
private boolean textViewErrorDelayVisible = false;
// Visibility of the Run button
private boolean btnExecuteVisible = 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_empty)
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 doExecute() {
// Check the entered data
if (!isPageValid()) {
return;
}
// clear previous answers
answers.clear();
dataAdapterAnswers.notifyDataSetChanged();
// reset the response counter to 0
nbAnswers = 0;
infoAnswers.setText("List of answers (0)");
// initialize activity
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// prepare the random task
beginWaiting(1);
// request random numbers
getRandomNumbersInBackground(nbRandomNumbers, a, b);
}
void getRandomNumbersInBackground(int nbRandomNumbers, int a, int b) {
// create the observable process
Observable<Response<Integer>> process = Observable.empty();
for (int i = 0; i < nbAleas; i++) {
process = process.mergeWith(mainActivity.getRandom(a, b));
}
// request the random numbers
executeInBackground(process, new Action1<Response<Integer>>() {
@Override
public void call(Response<Integer> response) {
// process the response
consumeRandomResponse(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 of +
nbReponses++;
infoReponses.setText(String.format("List of answers (%s)", nbReponses));
// analyze the response
// error?
if (response.getStatus() != 0) {
// display
showAlert(response.getMessages());
// Cancel
doCancel();
// return to UI
return;
}
// add the information to the list of responses
responses.add(0, String.valueOf(response.getBody()));
// refresh the responses
dataAdapterResponses.notifyDataSetChanged();
}
// Cancel ----------
@Click(R.id.btn_Cancel)
protected void cancel() {
if (isDebugEnabled) {
Log.d(className, "Cancelation requested");
}
// cancel asynchronous tasks
cancelRunningTasks();
}
private void beginWaiting(int nbRunningTasks) {
// start the timer
beginRunningTasks(nbRunningTasks);
// The [Cancel] button replaces the [Run] button
btnExecute.setVisibility(View.INVISIBLE);
btnCancel.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> responses;
private ArrayAdapter<String> dataAdapterAnswers;
private int numberOfAnswers = 0;
...
// lifecycle management ---------------------------------------------------------
@Override
public CoreState saveFragment() {
// current state of the view
Vue1FragmentState state = new Vue1FragmentState();
state.setTextViewErrorDelayVisible(textViewErrorDelay.getVisibility() == View.VISIBLE);
state.setTxtErrorRandomVisible(txtErrorRandom.getVisibility() == View.VISIBLE);
state.setTxtMsgWebServiceErrorUrlVisible(txtMsgWebServiceErrorUrl.getVisibility() == View.VISIBLE);
state.setTxtErrorIntervalVisible(txtErrorInterval.getVisibility() == View.VISIBLE);
state.setExecuteButtonVisible(executeButton.getVisibility() == View.VISIBLE);
state.setResponses(responses);
return state;
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// First visit?
if (previousState != null) {
Vue1FragmentState state = (Vue1FragmentState) previousState;
answers = state.getAnswers();
} else {
answers = new ArrayList<>();
}
// listView data source
dataAdapterAnswers = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, answers);
// number of answers
nbAnswers = answers.size();
}
@Override
protected void initView(CoreState previousState) {
// Link listview / adapter
listAnswers.setAdapter(dataAdapterAnswers);
// First visit?
if (previousState == null) {
// hide error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorInterval.setVisibility(View.INVISIBLE);
txtWebServiceErrorMessage.setVisibility(View.INVISIBLE);
textViewErrorDelay.setVisibility(View.INVISIBLE);
// buttons
btnCancel.setVisibility(View.INVISIBLE);
btnExecute.setVisibility(View.VISIBLE);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// previous state of the view
View1FragmentState state = (View1FragmentState) previousState;
// show/hide error messages
txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
txtErrorInterval.setVisibility(state.isTxtErrorIntervalVisible() ? View.VISIBLE : View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
textViewErrorDelay.setVisibility(state.isTextViewErrorDelayVisible() ? View.VISIBLE : View.INVISIBLE);
// buttons
btnCancel.setVisibility(state.isBtnExecuteVisible() ? View.INVISIBLE : View.VISIBLE);
btnExecute.setVisibility(state.isBtnExecuteVisible() ? View.VISIBLE : View.INVISIBLE);
// number of answers
infoAnswers.setText(String.format("List of answers (%s)", numberOfAnswers));
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// The [Run] button replaces the [Cancel] button
btnCancel.setVisibility(View.INVISIBLE);
btnExecute.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/title_view1"
android:id="@+id/textViewTitleView1"
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 {
// access to the session
ISession getSession();
// change view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum wait time for the server response
int TIMEOUT = 1000;
// wait time before executing the client 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;
// loading icon
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 {
// whether the fragment has been visited
protected boolean hasBeenVisited = false;
// state of the fragment's menu (if any)
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 to JSON must have the @JsonIgnore annotation
// Don't forget the getters and setters required for JSON serialization/deserialization
// number of fragments visited
private int numVisit;
// Fragment ID of type [PlaceholderFragment] displayed in the 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 {
// [DAO] layer
@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 methods from the parent class ---------------------------------------------------
...
}
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
// parent class methods -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// Continue the initializations started by the parent class
// session
this.session = (Session) super.session;
...
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// fragment number
final String ARG_SECTION_NUMBER = "section_number";
// initialize the fragment array
AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
int i;
for (i = 0; i < fragments.length - 1; i++) {
// create a fragment
fragments[i] = new PlaceholderFragment_();
// Pass arguments to the fragment
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 started by the parent class
// session
this.session = (Session) super.session;
// 1st tab
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("View 1");
tabLayout.addTab(tab);
// 2nd tab?
int numFragment = session.getNumFragment();
if (numFragment != -1) {
TabLayout.Tab tab2 = tabLayout.newTab();
tab2.setText(String.format("Fragment #%s", (numFragment + 1)));
tabLayout.addTab(tab2);
}
}
@Override
protected void navigateOnTabSelected(int position) {
// Fragment ID to display
int fragmentNumber;
switch (position) {
case 0:
// fragment number [View1Fragment]
numFragment = getFirstView();
break;
default:
// fragment number [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// display fragment
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="examples.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);
// recreate both tabs due to font issues with the titles
tabLayout.removeAllTabs();
tabLayout.addTab(tabLayout.newTab().setText("View1"), false);
tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment #%s", (i + 1))), false);
// The fragment number to display is set in the 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 {
// UI elements
@ViewById(R.id.editTextName)
protected EditText editTextName;
// event handler
@Click(R.id.buttonValider)
protected void validate() {
// display the entered name
Toast.makeText(activity, String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// fragment lifecycle -----------------------------------------------
private void initFragment() {
// nothing to do
}
// save fragment state
@Override
public CoreState saveFragment() {
// view state - 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) {
// First visit?
if (previousState == null) {
// display the visit number
showVisitNumber();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// display the visit number
showNumVisit();
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
// private methods -------------------------------------
// display number of visits
private void showVisitCount() {
// increment visit count
int visitCount = session.getVisitCount();
numVisit++;
session.setNumVisit(numVisit);
// display the visit count
Toast.makeText(activity, String.format("Visit #%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;
// constructors
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 {
// UI components
@ViewById(R.id.section_label)
protected TextView textViewInfo;
@ViewById(R.id.textView1)
protected TextView textView1;
// data
private String text;
// fragment ID
private static final String ARG_SECTION_NUMBER = "section_number";
// implementation of parent class methods ----------------------------
@Override
public CoreState saveFragment() {
// save the 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 displayed text
// increment visit count
int visitCount = session.getVisitCount();
numVisit++;
session.setNumVisit(numVisit);
// updated text
textViewInfo.setText(String.format("%s, visit %s", text, numVisit));
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateForSubmit, numvisit=%s, displayed text=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
}
}
@Override
protected void updateOnRestore(CoreState previousState) {
// restore the 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.



















































