1. Learning Android Programming
The PDF of the document is available |HERE|.
Examples from the document are available |HERE|.
1.1. Introduction
1.1.1. Contents
This document is a rewrite of several existing documents:
- Introduction to Android Tablet Programming Through Examples;
- Controlling an Arduino with an Android Tablet;
- Introduction to Android Tablet Programming Through Examples - Version 2
and introduces the following new features:
- Document 1 presented an architecture called AVAT (Activity-Views-Actions-Tasks) to facilitate asynchronous programming in an Android application. In this document, the standard RxJava library is used to manage asynchronous actions;
- Document 2 used the Eclipse IDE with an Android plugin. This document uses Android Studio;
- Document 3 is included as-is;
- Document 4 used the [Android Annotations] (AA) library with the IntelliJ IDEA Community Edition IDE. This document reproduces the entirety of Document 4 with the following differences:
- the IDE is now Android Studio;
- the build system is Gradle for all client or server projects (in document 4, Maven was sometimes used)
- asynchronous programming is implemented using the RxJava library (in Document 4, the AA library was used);
- this document explores areas not covered, or only briefly covered, in the previous documents:
- the concept of fragment adjacency;
- saving/restoring the activity and its fragments;
- the fragment lifecycle;
Finally, it presents the skeleton of an Android client communicating with a web service/JSON, in which we factor out a large number of elements commonly found in this type of client. This skeleton is used in all examples starting from Chapter 2. This is the truly innovative part of the document.
The following examples are presented:
Nature | |
Importing an existing Android project | |
A basic Android project | |
A basic [Android Annotations] project | |
Views and events | |
Navigation between views | |
Tab navigation | |
Using the [Android Annotations] library with Gradle | |
Managing fragments in an Android app | |
Revisited View Navigation | |
Two-layer architecture | |
Client/server architecture | |
Handling Asynchrony with RxJava | |
Data Entry Components | |
Using a View Pattern | |
The ListView Component | |
Using a Menu | |
Using a Parent Class for Fragments | |
Saving and restoring the state of the activity and fragments | |
Weather client | |
Skeleton of an Android client communicating with a web service / JSON. It factors in a large number of elements commonly found in this type of Android client. | |
Appointment management for a medical practice | |
Practical Exercise - Basic Payroll Management | |
Practical Exercise - Ordering Arduino Boards |
This document was used in the final year of the IstiA engineering school at the University of Angers [istia.univ-angers.fr]. This explains the sometimes somewhat unusual tone of the text. The two practical exercises are lab assignments for which only the broad outlines of the solution are provided. The solution must be developed by the reader.
The source code for the examples is available |HERE. To run these examples, you must follow the procedure in section 6.12.
This document is an introductory guide to Android programming. It is not intended to be exhaustive. It is primarily aimed at beginners.
The reference site for Android programming is at the URL [http://developer.android.com/guide/components/index.html]. That is where you should go to get an overview of Android programming.
1.1.2. Prerequisites
To get the most out of this document, you should have a solid understanding of the Java programming language.
1.1.3. Tools Used
The following examples have been tested in the following environment:
- Windows 10 Pro 64-bit machine;
- JDK 1.8;
- Android SDK API 23;
- Android Studio, version 2.1;
- Genymotion emulator, version 2.6.0;
To follow this document, you must install:
- a JDK (see section 6.8);
- the Genymotion Android emulator manager (see section 6.9);
- the Maven dependency manager (see section 6.10);
- the [Android Studio] IDE (see section 6.11);
1.2. Example-01: Importing an Android example
1.2.1. Creating the project
Let’s create our first Android project using Android Studio. First, let’s create an empty [examples] folder where all our projects will be stored:
![]() |
then create a project with Android Studio. We will first import one of the examples included with the IDE [1-5]:
![]() |

![]() | ![]() |
Importing the project may result in errors due to a mismatch between the environment used when the project was created and the one used here to run it. This is an opportunity to see how to resolve this type of error. Here, we have the following error:
![]() | ![]() |
The imported project is configured by the following [build.gradle] file [2]:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
apply plugin: 'com.android.application'
repositories {
jcenter()
}
dependencies {
compile "com.android.support:support-v4:23.3.0"
compile "com.android.support:support-v13:23.3.0"
compile "com.android.support:cardview-v7:23.3.0"
}
// The sample build uses multiple directories to
// keep boilerplate and common code separate from
// the main sample code.
List<String> dirs = [
'main', // main sample code; look here for the interesting stuff.
'common', // components that are reused by multiple samples
'template'] // boilerplate code generated by the sample template process
android {
compileSdkVersion 21
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 21
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
sourceSets {
main {
dirs.each { dir ->
java.srcDirs "src/${dir}/java"
res.srcDirs "src/${dir}/res"
}
}
androidTest.setRoot('tests')
androidTest.java.srcDirs = ['tests/src']
}
aaptOptions {
noCompress "pdf"
}
}
- The reported error is due to lines 31, 34–35: we do not have SDK 21. We replace this version with version 23, which we do have.
In the [build.gradle] file, Android Studio makes suggestions as shown below:
![]() |
To accept the suggestions, press [Alt-Enter] on the suggestion:
![]() |
You may also encounter an error regarding the Gradle version:
![]() |
This error stems from a mismatch between the Gradle version required by the project’s [build.gradle] file (version 2.10 on line 6 below):
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
and the one listed in the [<project>/gradle/wrapper/gradle-wrapper.properties] file:
#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https://services.gradle.org/distributions/gradle-2.8-all.zip
In line 6 above, replace 2.8 with 2.10.
To access the file [<project>/gradle/wrapper/gradle-wrapper.properties], use the project view:
![]() | ![]() | ![]() |
Once this is corrected, you can then compile the application [1], launch the Genymotion emulator [2], and run the project [3]:
![]() | ![]() | ![]() |
![]() |

Let’s stop the application:
![]() |
You can now close the project. We’re going to create a new one.
![]() |
1.2.2. A few notes on the IDE
1.2.2.1. Views
The Android Studio (AS) IDE offers different views for working with a project. We will mainly use two:
- the [Android] view [1]:
- the [Project] view [4];
![]() | ![]() |
![]() |
Most of the time, we will work with the [Android] view. When we clone a project into another, we will need the [Project] view.
1.2.2.2. Run Management
There are several ways to run, stop, or rerun an AS project. First, there are the buttons on the toolbar:
![]() | ![]() | ![]() |
The [Rerun] button [3] stops the project [2] and then restarts it [1].
1.2.2.3. Cache Management
Android Studio maintains a cache of the projects it manages to make the IDE as responsive as possible. With Android Studio version 2.1 (May 2016), this cache often did not reflect code changes that had just been made. In this case, you must invalidate the cache:
![]() | ![]() |
With Android 2.1 (May 2016), the previous step had to be performed multiple times, and sometimes that was not enough to resolve the detected issue. The solution was to disable [Instant Run]:
![]() | ![]() |
- in [3-4], everything was disabled;
In all of the following, we worked with this cache configuration and encountered no issues.
1.2.2.4. Log management
When running a project, logs are displayed in the Android Monitor:
![]() | ![]() |
In the [Android Monitor] tab [1], logs are displayed in the [logcat] tab [2]. The [3] button allows you to clear the logs. This button is useful when you want to view the logs for a specific action:
- clear the logs;
- on the Android device, perform the action for which you want the logs;
- the logs that appear are those related to the action performed;
There are several log levels [4]. By default, [Verbose] mode is selected. This means that logs from all levels are displayed. You can use [4] to select a specific level.
Logs are very useful for determining at which points during a project’s execution certain methods are executed. We will use them frequently. Let’s look at the code for the [MainActivity] class in the [Example-01] project:
![]() |
package com.example.android.pdfrendererbasic;
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
public class MainActivity extends Activity {
public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_real);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
.add(R.id.container, new PdfRendererBasicFragment(),
FRAGMENT_PDF_RENDERER_BASIC)
.commit();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_info:
new AlertDialog.Builder(this)
.setMessage(R.string.intro_message)
.setPositiveButton(android.R.string.ok, null)
.show();
return true;
}
return super.onOptionsItemSelected(item);
}
}
Above, the methods [onCreate, line 14] and [onCreateOptionsMenu, line 26] are methods of the parent class [Activity] (line 9). They are called at different points in the application's lifecycle. Sometimes they are executed multiple times. Even when reading the documentation, it can be difficult to tell whether a particular lifecycle method will execute before or after a method we’ve written ourselves. Yet this information is often important to know. We can therefore add logs as shown below:
public class MainActivity extends Activity {
public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity", "onCreate");
super.onCreate(savedInstanceState);
...
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("MainActivity", "onCreateOptionsMenu");
getMenuInflater().inflate(R.menu.main, menu);
...
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("MainActivity", "onOptionsItemSelected");
switch (item.getItemId()) {
...
}
}
- Lines 7, 14, and 21 use the [Log] class. This class allows you to write logs to the Android console [logcat]. Logs are classified into various levels (info, warning, debug, verbose, error). [Log.d] displays [debug]-level logs. Its first argument is the source of the log message. Indeed, various sources can send messages to the log console. To distinguish between them, we use this first argument. The second argument is the message to be written to the log console;
If we run the [Example-01] project again, we get the following logs:
05-28 08:37:12.709 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreate
05-28 08:37:12.778 23881-23923/com.example.android.pdfrendererbasic D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true
[ 05-28 08:37:12.781 23881:23881 D/ ]
HostConnection::get() New Host ...
05-28 08:37:12.967 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreateOptionsMenu
We can see that the [onCreate] method, which creates the Android activity, is executed before the [onCreateOptionsMenu] method, which creates the app menu.
Now, if we click on the menu option in the Android emulator [1]:
![]() |
the following log is added to the log console:
05-28 08:41:22.881 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onOptionsItemSelected
Moving forward, we will often add log statements to the Android code. Most of the time, we will not comment on them. They are simply there to encourage the reader to look at the log console in order to gradually understand the lifecycle of an Android application.
1.2.2.5. Managing the Emulator [Genymotion]
Sometimes, the Genymotion emulator crashes and cannot be restarted. This is because VirtualBox processes are still running in the Task Manager. Open the Task Manager [Ctrl-Alt-Del] and delete all VirtualBox processes:
![]() | ![]() |
Once this is done, restart the Genymotion emulator from Android Studio.
1.2.2.6. Managing the created APK binary
Compiling the project produces a binary with the .apk extension:
![]() | ![]() | ![]() |
There are two versions: one called [debug] and the other called [debug-unaligned]. You should use the first one; the other is an intermediate version. The .apk binary produced in [4] can be transferred directly to an emulator or an Android device. To transfer it to an emulator, simply drag and drop it onto the emulator with the mouse.
1.3. Example-02: A basic Android project
Let's create a new Android project using Android Studio [1-12]:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() | ![]() | ![]() |
In [13], we run the app. We then see the display shown in [14] on the Genymotion emulator.
1.3.1. Gradle Configuration
The created project is configured by the following [build.gradle] file:
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "examples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
}
This file was generated by the IDE using its configuration settings. It is a minimal file that we will gradually expand.
- lines 3–12: the characteristics of the Android application;
- lines 22–25: its dependencies. This is where we will primarily make changes based on the examples studied;
1.3.2. The application manifest
![]() |
The [AndroidManifest.xml] [1] file defines the characteristics of the Android application binary. Its content is as follows:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="examples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- line 3: the Android project package;
- line 10: the activity name;
These two pieces of information come from the entries made when the project was created:
![]() |
- Line 3 of the manifest (package) comes from entry [4] above. A number of classes are automatically generated in this package;
![]() |
- line 10 of the manifest (activity name) comes from entry [1] above;
Let’s return to the manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="examples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
![]() | ![]() | ![]() |
- Line 10: The application's main activity. It references the class [1] above;
- line 6: the application icon [2]. It can be changed;
- line 7: the application's label. It is located in the [strings.xml] file [3]:
<resources>
<string name="app_name">Example-02</string>
</resources>
The [strings.xml] file contains the strings used by the application. Line 2: the application name comes from the entry made when building the project [4]:
![]() |
- line 10: an activity tag. An Android application can have multiple activities;
- line 12: the activity is designated as the main activity;
- line 13: and it must appear in the list of apps that can be launched on the Android device.
1.3.3. The main activity
![]() | ![]() |
An Android app is based on one or more activities. Here, an activity [1] has been generated: [MainActivity]. An activity can display one or more views depending on its type. The generated [MainActivity] class is as follows:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
- line 6: the [MyActivity] class extends the Android [AppCompatActivity] class. This will be the case for all future activities;
- line 9: The [onCreate] method is executed when the activity is created. This happens before the view associated with the activity is displayed;
- line 10: the [onCreate] method of the parent class is called. This must always be done;
- line 11: the [activity_main.xml] file [2] is the view associated with the activity. The XML definition of this view is as follows:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
<Activity>
<TextView
android:text="Hello World!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- lines b-k: the layout manager. The default choice is the [RelativeLayout] type. In this type of container, components are positioned relative to one another (to the right of, to the left of, below, above);
- lines m-p: a [TextView] component used to display text;
- line n: the displayed text. It is not recommended to hardcode text directly in views. It is preferable to move this text to the [res/values/strings.xml] file [3]:
The displayed text will therefore be [Hello World!]. Where will it be displayed? The [RelativeLayout] container will fill the screen. The [TextView], which is its sole element, will be displayed at the top and left of this container, and thus at the top and left of the screen;
What does [R.layout.activity_main] on line 11 mean? Every Android resource (views, fragments, components, etc.) is assigned an identifier. Thus, a [V.xml] view located in the [res/layout] folder will be identified as [R.layout.V]. R is a class generated in the [app/build/generated] folder [1-3]:
![]() |
The [R] class is as follows:
...............
public static final class string {
public static final int abc_action_bar_home_description=0x7f060000;
public static final int abc_action_bar_home_description_format=0x7f060001;
public static final int abc_action_bar_home_subtitle_description_format=0x7f060002;
...
public static final int app_name = 0x7f060014;
}
public static final class layout {
public static final int abc_action_bar_title_item=0x7f040000;
public static final int abc_action_bar_up_container = 0x7f040001;
...
public static final int activity_main = 0x7f040019;
...
}
public static final class mipmap {
public static final int ic_launcher = 0x7f030000;
}
- line 14: the attribute [R.layout.activity_main] is the identifier for the view [res/layout/activity_main.xml];
- Line 7: The [R.string.app_name] attribute corresponds to the string ID [app_name] in the file [res/values/string.xml]:
- Line 19: The attribute [R.mipmap.ic_launcher] is the identifier for the image [res/mipmap/ic_launcher];
So, remember that when you reference [R.layout.activity_main] in the code, you are referencing an attribute of the [R] class. The IDE helps you identify the different elements of this class:
![]() | ![]() |
1.3.4. Running the Application
To run an Android application, we need to create a run configuration:
![]() | ![]() | ![]() |
- In [1], select [Edit Configurations];
- The project was created with an [app] configuration, which we will delete [2] to recreate it;
- in [3], create a new run configuration;
![]() |
- in [4], select [Android Application];

- in [5], select the [app] module from the drop-down list;
- In [6-8], keep the default values;
- In [7], the default activity is the one defined in the [AndroidManifest.xml] file (line 1 below):
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
- In [8], select [Show Chooser Dialog] to choose the device on which the app will run (emulator, tablet);
- In [9], specify that this choice should be saved;
- Confirm the configuration;
![]() |
- In [11], launch the emulator manager [Genymotion] (see section 6.9);
![]() |
- In [12], select a tablet emulator and launch it [13];
![]() | ![]() |
- in [14], run the [app] execution configuration;
- in [15], the runtime device selection form is displayed. Only one option is available here: the [Genymotion] emulator launched previously;
After a moment, the software emulator displays the following view:

1.3.5. The lifecycle of an activity
Let’s return to the code for the [MainActivity] activity:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
The [onCreate] method in lines 8–12 is one of the methods that can be called during an activity’s lifecycle. The Android documentation lists these methods:
![]() |
- [1]: The [onCreate] method is called when the activity starts. It is in this method that the activity is associated with a view and the references to its components are retrieved;
- [2-3]: The [onStart] and [onResume] methods are then called. Note that the [onResume] method is the last method to be executed before the currently running activity reaches state [4];
1.4. Example-03: Rewriting the [Example-02] project using the [Android Annotations] library
We will now introduce the [Android Annotations] library, which makes it easier to write Android applications. To do this, duplicate the [Example-02] example into [Example-03] by following steps [1-16].
![]() | ![]() |
- In [1], select the [Project] view to see the entire Android project;
![]() | ![]() |
![]() | ![]() |
![]() | ![]() | ![]() | ![]() |
Note: Between [14] and [15], we switched from the [Android] view to the [Project] view (see section 1.2.2.1).
We then modify the file [res/values/strings.xml] [17]:
![]() |
The [strings.xml] file is modified as follows:
<resources>
<string name="app_name">Example-03</string>
</resources>
Now, we run the new application, which has retained all the configuration from [Example-02]:
![]() | ![]() |
In [19], we get the same result as with [Example-02] but with a new name.
We will now introduce the [Android Annotations] library, which we will call AA for short. This library introduces new classes for annotating Android source code. These annotations will be used by a processor that will create new Java classes in the module; these classes will participate in the module’s compilation just like the classes written by the developer. We thus have the following build chain:
![]() |
First, we will add the dependencies for the AA annotation compiler (the processor mentioned above) to the [build.gradle] file:
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
}
- Lines 4–5 add the two dependencies that make up the AA library;
The [build.gradle] file is modified again to use a plugin called [android-apt], which splits the compilation process into two steps:
- processing of Android annotations, which generates new classes;
- compilation of all the project's classes;
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'
- line 8: version of the [android-apt] plugin that will be searched for in the central Maven repository (line 3);
- line 13: activation of this plugin;
At this point, verify that the [app] run configuration still works.
We will now introduce the first annotation from the AA library into the [MainActivity] class:
![]() |
The [MainActivity] class currently looks like this:
package examples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
We have already explained this code in section 1.3.3. We modify it as follows:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
- Line 7: The [@EActivity] annotation is an AA annotation (line 3). Its parameter is the view associated with the activity;
This annotation will generate a [MainActivity_] class derived from the [MainActivity] class, and this class will be the actual activity. We must therefore modify the project manifest [AndroidManifest.xml] as follows:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity_">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- line 11: the new activity;
Once this is done, we can compile the project [1]:
![]() | ![]() |
- In [2], we see the [MainActivity_] class generated in the [app/build/generated/source/apt/debug] folder;
The generated [MainActivity_] class is as follows:
//
// DO NOT EDIT THIS FILE.
// Generated using AndroidAnnotations 4.0.0.
//
// You can create a larger work that contains this file and distribute that work under terms of your choice.
//
package exemples.android;
import android.app.Activity;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import org.androidannotations.api.builder.ActivityIntentBuilder;
import org.androidannotations.api.builder.PostActivityStarter;
import org.androidannotations.api.view.HasViews;
import org.androidannotations.api.view.OnViewChangedNotifier;
public final class MainActivity_
extends MainActivity
implements HasViews
{
private final OnViewChangedNotifier onViewChangedNotifier_ = new OnViewChangedNotifier();
@Override
public void onCreate(Bundle savedInstanceState) {
OnViewChangedNotifier previousNotifier = OnViewChangedNotifier.replaceNotifier(onViewChangedNotifier_);
init_(savedInstanceState);
super.onCreate(savedInstanceState);
OnViewChangedNotifier.replaceNotifier(previousNotifier);
setContentView(R.layout.activity_main);
}
...
- lines 24-25: the [MainActivity_] class extends the [MainActivity] class;
We won’t attempt to explain the code of the classes generated by AA. They handle the complexity that the annotations aim to hide. But it can sometimes be helpful to examine it when you want to understand how the annotations you use are “translated.”
We can now run the [app] configuration again. We get the same result as before. We will now use this project as a starting point and duplicate it to introduce the key concepts of Android programming.
1.5. Example-04: Views and Events
1.5.1. Creating the Project
We will follow the procedure described for duplicating [Example-02] in [Example-03] in section 1.4:
We:
- duplicate the [Example-03] project into [Example-04] (after deleting the [app/build] folder from [Example-03]);
- load the [Example-04] project;
- change the project name in the file [app / res / values / strings.xml] (Android perspective);
- Delete the file [Example-04 / Example-04.iml] (Project view);
- compile and then run the project;
![]() | ![]() |
1.5.2. Building a view
We will now use the graphical editor to modify the view displayed by the [Example-04] project:
![]() | ![]() |
- In [1-4], create a new XML view;
- In [5], name the view;
- In [6], specify the view’s root tag. Here, we choose a [RelativeLayout] container. Within this component container, components are positioned relative to one another: “to the right of,” “to the left of,” “below,” “above”;
![]() |
The generated [vue1.xml] file [7] 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">
</RelativeLayout>
- Line 2: an empty [RelativeLayout] container that will occupy the entire width of the tablet (line 3) and its entire height (line 4);
![]() | ![]() |
- In [1], select the [Design] tab in the displayed [vue1.xml] view;
- in [2-4], switch to tablet mode;
![]() | ![]() | ![]() |
- in [5], set the scale to 1 for the tablet;
- In [6], select 'landscape' mode for the tablet;
- The screenshot [7] summarizes the choices made.
![]() | ![]() | ![]() |
- In [1], select a [Large Text] and drag it onto the [2] view;
- In [3], double-click the component;
- In [4], edit the displayed text. Rather than hard-coding it in the XML view, we will externalize it in the file [res/values/string.xml]
![]() | ![]() | ![]() |
- In [5], add a new value to the [strings.xml] file;
- in [8], assign an identifier to the string;
- in [9], assign the string value;
- in [10], the new view after validating the previous step;
![]() | ![]() | ![]() |
- after double-clicking the component, we change its ID [11];
- in [12], in the component properties, change the font size [50pt];
- in [13], the new view;
The file [vue1.xml] has changed 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="@string/view1_title"
android:id="@+id/textViewTitleView1"
android:layout_marginLeft="213dp" android:layout_marginStart="213dp"
android:layout_marginTop="50dp" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:textSize="50sp"/>
</RelativeLayout>
- The changes made in the GUI are on lines 10, 11, and 14. The other attributes of the [TextView] are either default values or result from the component’s positioning within the view;
- lines 7–8: the component’s size matches that of the text it contains (wrap_content) in both height and width;
- line 13: the top of the component is aligned with the top of the view (line 13), 50 pixels below (line 13);
- line 12: the left side of the component is aligned with the left side of the view (line 13), 213 pixels to the right (line 12);
Generally, the exact sizes of the left, right, top, and bottom margins will be set directly in the XML.
Following the same procedure, create the following view [1]:
![]() |
The components are as follows:
Positioning components relative to one another can be frustrating, as the graphical editor’s behavior is sometimes unpredictable. It may be better to use the components’ properties:
The [textView1] component must be placed 50 pixels below the title and 50 pixels from the left edge of the container:
![]() | ![]() | ![]() |
- in [1], the top edge of the component is aligned with the bottom edge of the [textViewTitreVue1] component at a distance of 50 pixels [3] (top);
- in [2], the left edge (left) of the component is aligned with the left edge of the container at a distance of 50 pixels [3] (left);
The [editTextNom] component must be placed 60 pixels to the right of the [textView1] component and aligned at the bottom with that same component;
![]() | ![]() |
- in [1], the left edge of the component is aligned with the right edge of the [textView1] component at a distance of 60 pixels [2] (left). It is aligned with the bottom edge (bottom:bottom) of the [textView1] component [1];
The [buttonValider] component must be placed 60 pixels to the right of the [editTextNom] component and aligned at the bottom with that same component;
![]() | ![]() |
- In [1], the left edge of the component is aligned with the right edge of the [editTextNom] component at a distance of 60 pixels [2] (left). It is aligned with the bottom edge of the [editTextNom] component (bottom:bottom) [1];
The [buttonVue2] component must be positioned 50 pixels below the [textView1] component and aligned to the left of that component;
![]() | ![]() |
- in [1], the left edge of the component is aligned with the left edge of the [textView1] component and is positioned below it (top:bottom) at a distance of 50 pixels [2] (top);
The generated XML file 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="@string/view1_title"
android:id="@+id/textViewTitleView1"
android:layout_marginTop="49dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/txt_name"
android:id="@+id/textView1"
android:layout_below="@+id/textViewTitleView1"
android:layout_alignParentLeft="true"
android:layout_marginLeft="50dp"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextName"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_validate"
android:id="@+id/buttonValider"
android:layout_alignBottom="@+id/editTextNom"
android:layout_toRightOf="@+id/editTextNom"
android:textSize="30sp"
android:layout_marginLeft="60dp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue2"
android:id="@+id/buttonVue2"
android:layout_below="@+id/textView1"
android:layout_alignLeft="@+id/textView1"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
</RelativeLayout>
This contains all the graphical elements. Another way to create a view is to edit this file directly. Once you get used to it, this can be faster than using the graphical editor.
- On line 38, there is information we haven’t shown. It is provided via the properties of the [editTextNom] component [1]:
![]() | ![]() |
All text comes from the following [strings.xml] [2] file:
<resources>
<string name="app_name">Example-04</string>
<string name="titre_vue1">View #1</string>
<string name="txt_nom">What is your name?</string>
<string name="btn_submit">Submit</string>
<string name="btn_view2">View #2</string>
</resources>
Now, let's modify the [MainActivity] so that this view is displayed when the app starts:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
- Line 7: The [vue1.xml] view is now displayed by the activity;
Modify the [AndroidManifest.xml] file as follows:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="examples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- Line 12: This configuration line prevents the keyboard from appearing as soon as the [vue1] view is displayed. This is because the view has an input field that has focus when the view is displayed. By default, this focus causes the virtual keyboard to appear;
Run the application and verify that the [view1.xml] view is indeed displayed:

1.5.3. Event Handling
Now let’s handle the click on the [Validate] button in the [View1] view:

The code for [MainActivity] changes as follows:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
// UI elements
@ViewById(R.id.editTextNom)
protected EditText editTextName;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity", "onCreate");
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews(){
Log.d("MainActivity", "afterViews");
}
// event handler
@Click(R.id.buttonValider)
protected void doValidate() {
// display the entered name
Toast.makeText(this, String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- Lines 17–18: We associate the field [protected EditText editTextNom] with the component identified by [R.id.editTextNom] in the visual interface. The field associated with the component must be accessible in the derived class [MainActivity_] and for this reason cannot have a [private] scope. The field identified by [R.id.editTextNom] comes from the view [vue1.xml]:
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextName"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
Note: Do not use accented characters in [id] identifiers. AA does not handle them correctly.
- Line 32: The annotation [@Click(R.id.buttonValider)] specifies the method that handles the 'Click' event on the button with ID [R.id.buttonValider]. This ID also comes from the view [vue1.xml]:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_valider"
android:id="@+id/buttonValider"
android:layout_alignBottom="@+id/editTextNom"
android:layout_toRightOf="@+id/editTextNom"
android:textSize="30sp"
android:layout_marginLeft="60dp"/>
- Line 35: displays the entered name:
- Toast.makeText(...).show(): displays text on the screen,
- the first parameter of makeText is the activity,
- the second parameter is the text to display in the dialog box that will be displayed by makeText,
- the third parameter is the duration of the displayed box: Toast.LENGTH_LONG or Toast.LENGTH_SHORT;
- line 26, the [@AfterViews] annotation marks the method to be executed once all fields annotated with [@ViewById] have been initialized. It is important to know when these fields are initialized. For example, can we use the reference from line 18 in the [onCreate] method? To answer this question, we have added logs;
Run the [Example-04] project and verify that something happens when you click the [Validate] button. We get the following logs:
We conclude that when the [onCreate] method runs, the fields annotated with [@ViewById] are not yet initialized. Again, beginner readers are encouraged to include this type of log in the methods that manage the application lifecycle.
1.6. Example-05: Navigating Between Views
In the previous project, the [View 2] button was not utilized. We propose to make use of it by creating a second view and demonstrating how to navigate between views. There are several ways to solve this problem. The approach proposed here is to associate each view with an activity. Another method is to have a single [AppCompatActivity] that displays [Fragment] views. This will be the method used in future applications.
1.6.1. Creating the Project
We duplicate the [Example-04] project into [Example-05]. To do this, we will follow the procedure described for duplicating [Example-02] into [Example-03] in Section 1.4, which was reproduced in Section 1.5.
![]() | ![]() |
1.6.2. Adding a second activity
To manage a second view, we will create a second activity. This activity will manage view #2. We are following a one-view-per-activity model here. Other models are possible.

- In [1-4], we create a new activity;

- in [5], the name of the class that will be generated;
- in [6], the name of the view (view2.xml) associated with the new activity;
![]() |
- in [7-8], the files affected by the previous configuration;
The activity [SecondActivity] is as follows:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.vue2);
}
}
- Line 11: The activity is associated with the view [vue2.xml];
The view [vue2.xml] is as follows:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="examples.android.SecondActivity">
</RelativeLayout>
This is currently an empty view with a [RelativeLayout] layout manager (line 2). On line 11, we can see that it has been associated with the new activity.
The Android module manifest [AndroidManifest.xml] has changed as follows:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="examples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity">
</activity>
</application>
</manifest>
Line 20: a second activity has been registered.
1.6.3. Navigating from View 1 to View 2
Let’s return to the code for the [MainActivity] class, which displays View 1. The transition to View 2 is not currently handled:
![]() |
We handle it as follows:
// navigate to View 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
// navigate to View 2 by passing it the name entered in View 1
// create an Intent
Intent intent = new Intent();
// associate this Intent with an activity
intent.setClass(this, SecondActivity.class);
// We associate information with this Intent
intent.putExtra("NAME", editTextName.getText().toString().trim());
// launch the [SecondActivity] activity by passing it the Intent
startActivity(intent);
}
- lines 2-3: the [navigateToView2] method handles the 'click' on the button identified by [R.id.buttonVue2] defined in the [vue1.xml] view:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue2"
android:id="@+id/buttonVue2"
android:layout_below="@+id/textView1"
android:layout_alignLeft="@+id/textView1"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
The comments describe the steps to follow for the view change:
- line 6: create an object of type [Intent]. This object will specify both the activity to launch and the information to pass to it;
- line 8: associate the Intent with an activity, in this case an activity of type [SecondActivity] that will be responsible for displaying view #2. Remember that the [MainActivity] displays view #1. So we have one view = one activity. We will need to define the [SecondActivity] type;
- Line 10: Optionally, add information to the [Intent] object. This information is intended for the [SecondActivity] that will be launched. The parameters for [Intent.putExtra] are (Object key, Object value). Note that the [EditText.getText()] method, which returns the text entered in the text field, does not return a [String] type but an [Editable] type. You must use the [toString] method to obtain the entered text;
- Line 12: Launch the activity defined by the [Intent] object.
Run the [Example-05] project and verify that you see View #2 (empty for now):
![]() | ![]() |
1.6.4. Building View #2
![]() | ![]() |
- In [1-2], we remove the [main.xml] view, which we no longer need, then we modify the [vue2.xml] view as follows:
![]() |
The components are as follows:
The XML file [vue2.xml] is as follows:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="examples.android.SecondActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/title_view2"
android:id="@+id/textViewTitleView2"
android:layout_marginTop="50dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textViewHello"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_below="@+id/textViewTitleView2"
android:layout_marginTop="50dp"
android:layout_marginLeft="50dp"
android:textSize="30sp"
android:text="Hello!"
android:textColor="#ffffb91b"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_view1"
android:id="@+id/buttonVue1"
android:layout_marginTop="50dp"
android:textSize="30sp"
android:layout_alignLeft="@+id/textViewHello"
android:layout_below="@+id/textViewHello"/>
</RelativeLayout>
Run the [Example-05] project and verify that you see the new view when you click the [View #2] button.
1.6.5. The [SecondActivity] activity
In [MainActivity], we wrote the following code:
// navigate to View 2
protected void navigateToView2() {
// navigate to View 2 by passing it the name entered in View 1
// create an Intent
Intent intent = new Intent();
// associate this Intent with an activity
intent.setClass(this, SecondActivity.class);
// attach information to this Intent
intent.putExtra("NAME", edtName.getText().toString().trim());
// launch the [SecondActivity] activity by passing it the Intent
startActivity(intent);
}
In line 9, we passed information to [SecondActivity] that wasn’t used. We’re now using it, and this happens in the code for [SecondActivity]:
![]() |
The code for [SecondActivity] changes as follows:
package exemples.android;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
// UI components
@ViewById
protected TextView textViewHello;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews() {
// Retrieve the intent if it exists
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// retrieve the name
String name = extras.getString("NAME");
if (name != null) {
// display it
textViewHello.setText(String.format("Hello %s!", name));
}
}
}
}
}
- line 11: we use the [@EActivity] annotation to indicate that the [SecondActivity] class is an activity associated with the [vue2.xml] view;
- lines 15–16: we retrieve a reference to the [TextView] component identified by [R.id.textViewBonjour]. Here, we did not write [@ViewById(R.id.textViewBonjour)]. In this case, AA assumes that the component’s identifier is identical to the annotated field, here the [textViewBonjour] field;
- line 23: the [@AfterViews] annotation marks a method that must be executed after the fields annotated with [@ViewById] have been initialized. In the [OnCreate] method (line 19), these fields cannot be used because they have not yet been initialized. In the [Example-05] project, we switch from one activity to another, and it was initially unclear whether the method annotated with [@AfterViews] would be executed once during the initial instantiation of the activity or every time the activity is started. Tests showed that the second hypothesis was correct;
- line 26: the [AppCompatActivity] class has a [getIntent] method that returns the [Intent] object associated with the activity;
- line 28: the [Intent.getExtras] method returns a [Bundle] object, which is a kind of dictionary containing information associated with the activity’s [Intent] object;
- line 31: we retrieve the name stored in the activity's [Intent] object;
- line 34: we display it.
Reminder: fields annotated with the [@ViewById] annotation must not contain accented characters.
Let’s go back to the [SecondActivity] class. Because we wrote:
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
AA will generate a [SecondActivity_] class derived from [SecondActivity], and this class will be the actual activity. This leads us to make changes in:
[MainActivity]
// navigate to view #2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
..
// associate this Intent with an activity
intent.setClass(this, SecondActivity.class);
...
}
- In line 6, we must replace [SecondActivity] with [SecondActivity_];
[AndroidManifest.xml]
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="examples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity_">
</activity>
</application>
</manifest>
- On line 20, replace [SecondActivity] with [SecondActivity_];
Test this new version. Type a name in view #1 and verify that view #2 displays it correctly.
![]() | ![]() |
1.6.6. Navigating from View #2 to View #1
To navigate from View #2 to View #1, we will follow the procedure seen earlier:
- Place the navigation code in the [SecondActivity] activity that displays View 2;
- write the [@AfterViews] method in the [MainActivity] that displays View 1;
The code for [SecondActivity] changes as follows:
@Click(R.id.buttonVue1)
protected void navigateToView1() {
// create an Intent for the [MainActivity]
Intent intent1 = new Intent();
intent1.setClass(this, MainActivity.class);
// Retrieve the Intent for the current activity [SecondActivity]
Intent intent2 = getIntent();
if (intent2 != null) {
Bundle extras2 = intent2.getExtras();
if (extras2 != null) {
// Set the name in the Intent of [MainActivity]
intent1.putExtra("NAME", extras2.getString("NAME"));
}
// launch [MainActivity]
startActivity(intent1);
}
}
- lines 1-2: associate the [navigateToView1] method with a click on the [btn_vue1] button;
- line 4: we create a new [Intent];
- line 5: associated with the [MainActivity_] activity;
- line 7: retrieve the Intent associated with [SecondActivity];
- line 9: retrieve the information from this Intent;
- line 12: the [NAME] key is retrieved from [intent2] and placed in [intent1] with the same associated value;
- line 15: the [MainActivity_] activity is launched.
In the code for [MainActivity], we add the following [@AfterViews] method:
@AfterViews
protected void afterViews() {
// retrieve the intent if it exists
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// retrieve the name
String name = extras.getString("NAME");
if (name != null) {
// display it
editTextName.setText(name);
}
}
}
}
Make these changes and test your app. Now, when you return from View 2 to View 1, the name you originally entered should be displayed, which was not the case until now.
![]() | ![]() |
1.6.7. Activity Lifecycle
In Section 1.3.5, we introduced the lifecycle of an activity. Here we have two activities, and we switch between them during execution. These activities contain two methods—[onCreate] and [afterViews]—and it’s not immediately clear when one is called relative to the other. It’s important to know this. To find out, we’ll add logs to both activities:
So in the [MainActivity] class, we write:
// constructor
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity", "onCreate");
...
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
}
}
- lines 2–4: we want to know if the [MainActivity] class is instantiated once or multiple times;
- line 8: we want to know if the [onCreate] method is called once or multiple times;
- line 14: we want to know if the [afterViews] method is called once or multiple times;
We do exactly the same thing in the [SecondActivity] class.
When the app starts, we see the following logs:
The [onCreate, afterViews] methods of the first activity were executed in this order. When you click the [View #2] button, the new logs are as follows:
The [onCreate, afterViews] methods of the second activity were executed in this order. When you click the [View #1] button, the new logs are as follows:
The [MainActivity] class is therefore instantiated again. When you click the [View #2] button, the new logs are as follows:
The [SecondActivity] class is therefore instantiated again.
Both activities are therefore systematically recreated whenever the activity is changed.
We will now explore an architecture with a single activity capable of managing multiple views called fragments. The activity and the views will be instantiated only once, unlike the previous method where an activity could be instantiated multiple times.
1.7. Example-06: Tab Navigation
Here we will explore tabbed interfaces. The example is complex but introduces all the elements we will use later: single activity, fragment manager (views), fragment container, navigation between fragments. The concept of tabs differs from that of fragments and is secondary to what we want to demonstrate in this example.
1.7.1. Creating the project
We create a new project:
![]() | ![]() |
![]() |
![]() |
- in [7], select a tabbed activity (Tabbed Activity);
![]() |
- in [10-14], keep the default values;
- in [15], select tabs with a title bar;
The resulting project is as follows:
![]() | ![]() |
- in [1], the activity;
- in [2], the views;
A runtime configuration [app], named after the module, has been automatically created [2b]:
![]() |
You can run it. A window with three tabs [3-6] then appears:

1.7.2. Gradle Configuration
The project [Example-06] was generated with the following [build.gradle] file:
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "examples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
}
There is a new element compared to what we have seen before: line 25. This library is required for the new components used by the generated application.
1.7.3. The [activity_main] view
![]() |
The [activity_main] view is the view associated with the project's [MainActivity]. In [design] mode, the view looks like this:

It contains the following components:
![]() |
- [main_content] is the entire view;
- [appbar] (red box, 1) is the application bar. It contains two components:
- [toolbar] (yellow box 4) is the toolbar;
- [tabs] (orange box 5) is the tab title bar;
- [container] (green box, 2) can hold various fragments. A fragment is a view. Thus, the same activity can display multiple views (fragments) in this container;
- [fab] (component 3) is called a floating component;
In [text] mode, the code 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"
<layout id="exemples.android.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
We see the elements described earlier:
- lines 2–49: the definition of the [main_content] component (line 5), which constitutes the entire view. We can see that it is a [CoordinatorLayout] layout (line 2);
- lines 11–33: the [appbar] container (line 12). This is an [AppBarLayout] (line 11);
- lines 18–24: the [toolbar] component (line 19) of type [Toolbar] (line 18);
- lines 28–31: the [tabs] container (line 29). This is a layout of type [TabLayout] (line 28). It will display the tab titles;
- lines 35–39: the [container] component (line 36). This container displays the different views of the activity;
- lines 41–47: the [fab] component (line 42) of type [FloatingActionButton] (line 41). This is a button that can be clicked. By default, it is positioned at the bottom right of the entire view;
We won’t try to understand the meaning of all these components’ attributes. We’ll use them as-is. It’s through experience—and often in [design] mode—that we discover their roles. In this mode, we find that components have dozens of attributes. Generally, only some are initialized, while the others retain their default values.
Let’s clarify a few points, however. Most of the values configuring the different views are gathered in the [res/values] folder:
![]() |
These values are referenced on lines 15–16, 23, 39, and 46 of the [activity_main.xml] file. Let’s take an example:
- line 15:
android:paddingTop="@dimen/appbar_padding_top"
The [@dimen] annotation refers to the [res/values/dimens.xml] file:
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="appbar_padding_top">8dp</dimen>
</resources>
Line 15 of the [activity_main.xml] file refers to line (f) above;
Similarly, the annotation:
- [@string] refers to the resource file [res/values/strings.xml];
- [@color] refers to the resource file [res/values/colors.xml];
- [@style] refers to the resource file [res/values/styles.xml];
1.7.4. The Activity
![]() |
The code generated for the activity matches the complexity of the view described above: it is complex. We will analyze it in several steps.
1.7.4.1. Managing fragments and tabs
The code in [MainActivity] related to fragments and tabs is as follows:
package exemples.android;
import android.support.design.widget.TabLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// the fragment manager
private SectionsPagerAdapter mSectionsPagerAdapter;
// the fragment container
private ViewPager mViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// view
setContentView(R.layout.activity_main);
// toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// the fragment manager
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e., fragment #i in the fragment container is fragment #i returned by the fragment manager
mViewPager = (ViewPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// the tab bar is also associated with the fragment container
// i.e., tab #i displays fragment #i from the container
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
}
// a fragment
public static class PlaceholderFragment extends Fragment {
...
}
// the fragment manager
// this is where we request the fragments to be displayed in the main view
// must define the [getItem] and [getCount] methods—the others are optional
public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
}
}
- line 28: Android provides a view container of type [android.support.v4.view.ViewPager] (line 12). This container must be provided with a view or fragment manager. The developer is responsible for providing it;
- line 25: the fragment manager used in this example. Its implementation is on lines 61–63;
- line 31: the method executed when the activity is created;
- line 35: the [activity_main.xml] view is associated with the activity;
- line 37: we retrieve the reference to the [toolbar] component of the view via its identifier;
- line 38: this toolbar becomes the activity’s action bar (an Android concept);
- line 40: the fragment manager is instantiated. The constructor parameter is the Android class [android.support.v4.app.FragmentManager] (line 10);
- line 44: we retrieve the reference to the fragment container from the [activity_main.xml] view via its ID;
- line 45: the fragment manager is linked to the fragment container. This means that when the fragment container is asked to display fragment #i, it will request it from the fragment manager;
- line 48: we retrieve a reference to the tab bar via its identifier;
- line 49: the tab manager is associated with the fragment container. This means that when tab #i is clicked, the container will display fragment #i. The association between the tab manager and the fragment container eliminates the need for tab management. Thus, we do not need to define an event handler for clicking on a tab. The association with the fragment container provides this by default. We will see an example where there are more fragments than tabs. In this case, we do not make this association.
The fragment handler [SectionsPagerAdapter] is as follows:
// the fragment handler
// this is what we call to retrieve the fragments to display in the main view
// must define the [getItem] and [getCount] methods—the others are optional
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// fragment position
@Override
public Fragment getItem(int position) {
// instantiate a [PlaceHolder] fragment and return it
return PlaceholderFragment.newInstance(position + 1);
}
// returns the number of fragments managed
@Override
public int getCount() {
return 3;
}
// optional - gives a title to the managed fragments
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return "SECTION 1";
case 1:
return "SECTION 2";
case 2:
return "SECTION 3";
}
return null;
}
}
}
- The fragments displayed by an app depend on the app itself. The fragment manager is defined by the developer;
- line 5: the fragment manager extends the Android class [android.support.v4.app.FragmentPagerAdapter]. The constructor is provided for us. We must define at least the following two methods:
- int getCount(): returns the number of fragments to manage;
- Fragment getItem(i): returns fragment #i;
The CharSequence getPageTitle(i) method, which returns the title of fragment #i, is optional. Because the tab manager has been associated with the fragment manager, the title of tab #i will be the title of fragment #i. Thus, the titles in lines 27–33 will be the tab titles;
- Lines 18–21: getCount returns the number of managed fragments, in this case three;
- lines 11–15: getItem(i) returns fragment #i. Here, all fragments will be of the same type, [PlaceholderFragment];
- lines 24–35: getPageTitle(int i) returns the title of fragment #i;
1.7.4.2. The displayed fragments
![]() |
The activity’s fragments are all of the same type here and are all associated with the following XML view [fragment_main]:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="examples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- lines 1–16: a [RelativeLayout] layout;
- lines 11-14: the single component of the view (fragment): a [TextView] identified by [section_label];
In [MainActivity], the managed fragments are of the following [PlaceholderFragment] type:
// a fragment
public static class PlaceholderFragment extends Fragment {
// text displayed in the fragment
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
}
// returns a fragment with information: the fragment number passed as a parameter
public static PlaceholderFragment newInstance(int sectionNumber) {
// fragment
PlaceholderFragment fragment = new PlaceholderFragment();
// embedded info
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// result
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// [fragment_main] view is instantiated
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
// the [TextView] is found
TextView textView = (TextView) rootView.findViewById(R.id.section_label);
// its content is modified
textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
// return the view
return rootView;
}
}
- line 2: the [PlaceholderFragment] class extends the Android [Fragment] class. This is generally always the case;
- line 2: the [PlaceholderFragment] class is static. Its [newInstance] method (line 10) allows you to obtain instances of type [PlaceholderFragment];
- lines 10–19: the [newInstance] method creates and returns an object of type [PlaceholderFragment];
- lines 14–16: the fragment is created with an argument;
A fragment must define the [onCreateView] method on line 22. This method must return the view associated with the fragment.
- Line 25: The view [fragment_main.xml] is associated with the fragment;
- Line 27: This view contains a [TextView] component, whose reference is retrieved via its ID;
- line 29: text is displayed in the [TextView];
- [getString] is a method of the parent class [AppCompatActivity];
- the first argument is a component ID. [R.string.section_format] refers to the ID of the component identified by [section_format] in the file [res/values/strings.xml] (line 4 below):
<resources>
<string name="app_name">Example-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
- (continued)
- line (d) above %1$d indicates that argument #1 (%1) must be formatted as an integer ($d);
- the second argument of [getString] is the value to be assigned to argument $1 in line (d) above;
- [getArguments] returns the reference to the fragment's argument bundle. It is important to note here that each argument was created with the following bundle (lines f-h):
// returns a fragment with information: the fragment number passed as a parameter
public static PlaceholderFragment newInstance(int sectionNumber) {
// fragment
PlaceholderFragment fragment = new PlaceholderFragment();
// embedded info
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// result
return fragment;
}
- (continued)
- getArguments().getInt(ARG_SECTION_NUMBER) will therefore return the value [sectionNumber] from lines (g) and (b) above;
- line 31: we return the view thus created;
1.7.4.3. Menu Management
In the generated application, there is a menu:
![]() |
The contents of the [menu_main.xml] file are 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="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- lines 1-9: the menu;
- lines 5-8: a menu item identified by [action_settings] (line 5);
- line 6: the label for the menu option. It is found in the file [res/values/strings.xml] (line (c) below:
<resources>
<string name="app_name">Example-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
The code above corresponds to the following visual (the menu is at the top right of the Android runtime window):
![]() | ![]() |
This menu is handled as follows in the [MainActivity] activity:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Back button, as long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
- Lines 1–6: This method is called when the system is ready to create the application menu. The input parameter [Menu menu] is an empty menu that does not yet have any options;
- line 4: the file [res/menu/menu_main.xml] is used. The [Menu menu] object passed as a parameter is assigned the menu options defined in this file;
- line 5: it is indicated that the menu has been created;
- lines 8–21: the [onOptionsItemSelected] method is executed whenever a menu option is clicked;
- line 13: the reference of the clicked menu option;
- lines 16–18: if the clicked option is the one with the identifier [action_settings], nothing is done and it is indicated that the event has been handled (line 17);
- line 20: the event is passed to the parent class;
To better see what happens with this menu, we add logs to the previous code:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("menu", "creating menu");
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Back button, as long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
// parent
return super.onOptionsItemSelected(item);
}
1.7.4.4. The floating button
The generated view has a floating button:
![]() |
This component is defined in the main view [activity-main.xml]:
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
Line 7 references an image provided by the Android framework, specifically an envelope.
This component is handled in the [MainActivity] class as follows:
// floating button
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
- line 2: retrieve the reference of the floating button in the view associated with the activity (activity_main);
- lines 3–9: we assign a handler to it to handle clicks on it;
- line 6: the [Snackbar] class allows you to display temporary messages on the view using its [Snackbar.make] method. The first argument is a view from which [Snackbar] will look for a parent view in which to display the message. Here, [view] is the view of the clicked envelope (line 5). The parent view that will be found is the [activity_main] view. The second argument is the message to display. The third argument is the display duration (SHORT or LONG);
- line 7: you can click on the displayed message to trigger an action. Here, no action is associated with clicking the message. Finally, the [show] method displays the message;
Clicking the floating button produces the following visual result:
![]() |
1.7.5. Running the project
Now that we have explained the details of the generated code, we can better understand its execution:

When you click on tab #i, fragment #i is displayed in the view container. This is evident from the text displayed in [4]. You can also see that you can switch between tabs by swiping the view to the right or left with the mouse. We will see that this behavior can be controlled.
When you click the menu option at [6], you get the following logs:
![]() |
1.7.6. Fragment Lifecycle
![]() | ![]() |
- In [1], we see that the [onCreateView] method and the subsequent methods are executed when the fragment is first displayed and every time the activity needs to redraw it;
To track the lifecycle of the activity and fragments, we add the following logs to the [MainActivity] code:
// constructor
public MainActivity(){
Log.d("MainActivity", "constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity", "onCreate");
// parent
super.onCreate(savedInstanceState);
...
}
// a fragment
public static class PlaceholderFragment extends Fragment {
// text displayed in the fragment
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
// returns a fragment with information: the fragment number passed as a parameter
public static PlaceholderFragment newInstance(int sectionNumber) {
Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
// fragment
PlaceholderFragment fragment = new PlaceholderFragment();
...
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("PlaceholderFragment", String.format("newInstance %s", getArguments().getInt(ARG_SECTION_NUMBER)));
...
}
}
}
We run the project again. The first logs are as follows:
- line 1: creating the activity;
- line 2: execution of its [onCreate] method;
- lines 3-4: instantiation of fragment #1;
- lines 5-6: instantiating fragment #2;
- line 7: initializing fragment #2;
- line 8: initialization of fragment #1;
- line 9: creation of the activity menu;
Here, we must recall the code responsible for creating the fragments:
// the fragment manager
// this is what we call to retrieve the fragments to display in the main view
// must define the [getItem] and [getCount] methods—the others are optional
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// fragment position
@Override
public Fragment getItem(int position) {
// instantiate a [PlaceHolder] fragment and return it
return PlaceholderFragment.newInstance(position + 1);
}
...
- lines 11–15: a fragment is instantiated by [newInstance] each time the fragment container requests one;
The logs above show that the first two fragments have been instantiated and initialized.
Now, let’s click on tab #2. The new logs are as follows:
- Lines 1–3: Fragment #3 is instantiated and initialized. Remember that Fragment #2 is the one being displayed;
Now, let’s click on tab #3. There are no logs here. This is likely because fragment #3, which is to be displayed, had already been instantiated. Now, let’s return to tab #1. The logs are as follows:
Fragment #1 is not instantiated again, but its [onCreateView] method is executed again. This behavior occurs for the other two fragments as well.
From these logs, we can conclude that:
- the activity was instantiated and initialized once;
- each fragment was instantiated once;
- the [onCreateView] method of each fragment was executed multiple times;
What you need to know—and what the logs confirm—is that by default, when fragment #i is displayed, fragments i-1 and i+1 are instantiated, if they aren’t already. This explains, for example, why at startup, even though fragment #1 should be displayed, fragments 1 and 2 were instantiated and initialized. The logs also show that the [getItem(i)] method is called only once, even if fragment #i is displayed multiple times. Thus, it appears that the fragment container [ViewPager], which is supposed to display the [SectionsPagerAdapter] fragment #i, requests it once from the fragment manager [ ]. After that, it does not request it again and continues to use the one it obtained.
Finally, the logs provide information about the fragments’ [onCreateView] method:
- at startup, fragments 1 and 2 were instantiated and their [onCreateView] method executed;
- When switching from Fragment 1 to Fragment 2, Fragment 2's [onCreateView] method is not re-executed. Therefore, it cannot be used to update Fragment 2. However, the user may have performed an operation in Fragment 1 whose result should be displayed by Fragment 2. We see that the [onCreateView] method cannot be used to update Fragment 2. We will need to find another solution;
1.8. Example-07: Example-06 rewritten using the [AA] library
1.8.1. Creating the project
We will duplicate the [Example-06] project into [Example-07] to introduce Android annotations into the latter. To do this, follow the procedure in section 1.4. We obtain the following result:
![]() | ![]() |
1.8.2. Gradle Configuration
![]() |
We update the [build.gradle] file as follows:
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 {
applicationId "examples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
We have added the configuration required to use the [Android Annotations] library (see section 1.4).
1.8.3. Adding the first AA annotations
We will create AA annotations in [MainActivity]:
![]() |
The [MainActivity] class changes as follows:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// the fragment manager
private SectionsPagerAdapter mSectionsPagerAdapter;
// the fragment container
@ViewById(R.id.container)
protected MyPager mViewPager;
// the tab manager
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// the floating button
@ViewById(R.id.fab)
protected FloatingActionButton fab;
// constructor
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
// toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// the fragment manager
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e., fragment #i in the fragment container is fragment #i provided by the fragment manager
mViewPager.setAdapter(mSectionsPagerAdapter);
// the tab bar is also associated with the fragment container
// i.e., tab #i displays fragment #i from the fragment container
tabLayout.setupWithViewPager(mViewPager);
// floating button
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
- line 1: the [@EActivity] annotation makes [MainActivity] a class managed by AA. Its parameter [R.layout.activity_main] is the identifier of the [activity_main.xml] view associated with the activity;
- lines 11-12: the component identified by [R.id.tabs] is injected into the [tabLayout] field. This is the tab manager;
- lines 14–15: the component identified by [R.id.fab] is injected into the [fab] field. This is the floating button;
- lines 23–50: the code that was previously in the [onCreate] method is moved to a method with any name but annotated with [@AfterViews] (line 23). In the method annotated in this way, we can be sure that all visual interface components annotated with [@ViewById] have been initialized;
- We have also added logs to view the activity’s lifecycle;
Remember that the [@EActivity] annotation will generate a [MainActivity_] class, which will be the project’s actual activity. Therefore, you must modify the [AndroidManifest.xml] file as follows:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="examples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- Line 12: the new activity.
At this point, run the project again and verify that you still get the interface with tabs.
1.8.4. Rewriting the fragments
We will review how fragments are managed in the project. For now, the [PlaceholderFragment] class is a static inner class of the [MainActivity] activity. We will return to a more common use case, where fragments are defined in external classes. Additionally, we are introducing AA annotations for fragments.
The [Example-07] project evolves as follows:
![]() |
Above, we see the [PlaceholderFragment] class, which has been moved outside the [MainActivity] class. It is rewritten as follows:
package exemples.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// A fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// visual interface component
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// fragment number
private static final String ARG_SECTION_NUMBER = "section_number";
// constructor
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("PlaceholderFragment", String.format("afterViews %s", getArguments().getInt(ARG_SECTION_NUMBER)));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("PlaceholderFragment", String.format("onCreateView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onResume() {
Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// parent
super.onResume();
// display
if (textViewInfo != null) {
Log.d("PlaceholderFragment", String.format("onResume setText %s", getArguments().getInt(ARG_SECTION_NUMBER)));
textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}
}
}
- line 15: the fragment is annotated with the [@EFragment] annotation, whose parameter is the identifier of the XML view associated with the fragment, in this case the [fragment_main.xml] view;
- lines 19-20: inject into the [textViewInfo] field the reference to the component in [fragment_main.xml] identified by [R.id.section_label], which is of type [TextView] (line (l) below):
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="examples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- lines 42–52: The [onResume] method is executed before the view associated with the fragment is displayed. It can be used to update the user interface that will be displayed;
- line 47: you must call the method of the same name in the parent class;
- line 49: It is unclear whether the [onResume] method can be executed before the field on line 20 is initialized. The logs set up to track the fragment’s lifecycle will tell us. For now, as a precaution, we perform a null check;
- Line 51: We update the information in the [textViewInfo] field with the integer argument passed to the fragment during its creation;
The [MainActivity] class loses its inner class [PlaceholderFragment] and sees its fragment manager evolve as follows:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// the fragments
private Fragment[] fragments;
// number of fragments
private static final int FRAGMENTS_COUNT = 3;
// fragment number
private static final String ARG_SECTION_NUMBER = "section_number";
// constructor
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
// initialize the fragment array
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length; 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);
}
}
// fragment #position
@Override
public Fragment getItem(int position) {
Log.d("MainActivity", String.format("getItem[%s]", position));
return fragments[position];
}
// returns the number of managed fragments
@Override
public int getCount() {
return fragments.length;
}
// optional - sets a title for the managed fragments
@Override
public CharSequence getPageTitle(int position) {
return String.format("Tab #%s", (position + 1));
}
}
- line 4: the fragments are placed in an array;
- lines 16–23: the fragment array is initialized in the constructor. They are of type [PlaceholderFragment_] (line 18) and not [PlaceholderFragment]. The [PlaceholderFragment] class has indeed been annotated with an AA annotation and will generate a [PlaceholderFragment_] class derived from [PlaceholderFragment], and it is this class that the activity must use. Each created fragment is passed an integer argument that will be displayed by the fragment;
- lines 42–45: we have changed the fragment titles. Since these are also the tab titles, we should see a change in the tab bar;
Let’s compile [Make] [1] this project:
![]() | ![]() |
- in [2], we can see that the classes generated by the AA library are located in the [app / build / generated / source / apt / debug] folder (you must be in the [Project] perspective to see [2]);
Run the [Example-07] project and verify that it still works.
1.8.5. Reviewing the logs
When the application is launched, the logs are as follows:
- line 1: construction of the single activity;
- line 2: the activity's [afterViews] method: its fields annotated with [@ViewById] are initialized;
- lines 3-5: construction of the three fragments;
- lines 6-7: the fragment container [ViewPager] requests the first two fragments;
- lines 8-9: methods of fragment 2;
- lines 10–11: methods of fragment 1;
- lines 12–13: [onResume] method of fragment 1;
- lines 14–15: [onResume] method of fragment 2;
- line 16: creation of the activity menu;
Note that this answers a question asked earlier: the [onResume] method of fragment 1, for example (line 12), runs after the fragment’s [afterViews] method (line 11). Therefore, when the [onResume] method runs, it can use the fields annotated with [@ViewById]. We can now write the [onResume] method as follows:
@Override
public void onResume() {
Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// parent
super.onResume();
// display
textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}
Now let's switch from tab 1 to tab 2. The new logs are as follows:
- line 1: the fragment container [ViewPager] requests fragment #3;
- lines 2-3: methods of fragment #3. Note that this fragment was instantiated when the application started;
- lines 4-5: the [onResume] method of fragment #3 is executed. Note that fragment #2 is currently displayed;
Now let’s switch from tab 2 to tab 3. There are no logs. Therefore, none of the methods [onCreateView, afterViews, onResume] of fragment #3 are executed. It correctly displays the text [Hello World from section:3] solely because this text had already been created in the previous step when Fragment #2 was displayed. Recall that at that step, the [onResume] method of Fragment #3 had been executed. We can see here that, just like the [onCreateView] method, the [onResume] method cannot be used to update Fragment 3. If we had needed to change the text displayed by the fragment, neither of these two methods could have done so.
Now, let’s return from tab #3 to tab #1. The logs are then as follows:
We can see that all methods in Fragment 1 have been executed. We can see that the getItem method was not called. As mentioned, this method is called only once for each fragment;
Now, let’s switch from tab 1 to the adjacent tab 2. We get the following logs:
Surprising, isn’t it? All methods of fragment #3 are re-executed.
To understand these phenomena, remember that by default, when the fragment container displays fragment i, it initializes fragments i-1, i, and i+1. Let’s review the logs in light of this information.
First, the logs when the app starts:
Because the fragment container will display fragment 1, fragments 1 and 2 are initialized (lines 8–15).
We now switch from tab 1 to tab 2:
Because the fragment container will display fragment 2, fragments 1, 2, and 3 must be initialized. Fragments 1 and 2 have already been initialized in the previous step. Fragment 3 is initialized in lines 2–5.
We switch from tab 2 to tab 3. There are no logs. Because the fragment container will display fragment 3, fragments 2 and 3 must be initialized. However, since the previous step, they already are. What we don’t see here is that fragment 1, which is not adjacent to fragment 3, loses its state, which is not retained in memory.
We switch from tab 3 to tab 1. The logs are as follows:
Because the fragment container will display fragment 1, fragment 2 must also be initialized. It has been initialized since the previous step. In that same step, the state of fragment 1 was lost. It is therefore reset in lines 1–4. What we don’t see here is that fragment 3, which is not adjacent to fragment 1, loses its state, which is then not retained in memory.
When switching from tab 1 to the adjacent tab 2, we get the following logs:
Because the fragment container will display fragment 2, fragments 1, 2, and 3 must be initialized. Fragments 1 and 2 have already been initialized in the previous step. Fragment 3 is initialized in lines 1–4.
What have we learned?
- that the default fragment management is very specific and that you need to understand it if you don’t want to pull your hair out. We can change this management mode, and we’ll do that a little later;
- that with this default handling, neither of the [onCreateView, onResume] methods can be used to update the fragment that will be displayed because we cannot be sure they will be executed;
1.8.6. onDestroyView
The [onDestroyView] method is part of the fragment lifecycle (see section 1.7.6):
![]() | ![]() |
We see that in a fragment’s lifecycle:
- the [onCreateView] method may be executed multiple times;
- before returning to the [onCreateView] method later, there is necessarily a call to the [onDestroyView] method [2];
We will insert these methods into the fragments to better track their lifecycle. The fragment code becomes the following:
package exemples.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// A fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
...
@Override
public void onDestroyView() {
// log
Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// parent
super.onDestroyView();
}
}
Let's run the app. The first logs are as follows:
- line 1: construction of the single activity;
- line 2: the activity's [afterViews] method: its fields annotated with [@ViewById] are initialized;
- lines 3-5: construction of the three fragments;
- lines 6-7: the fragment container [ViewPager] requests the first two fragments;
- lines 8-9: the view for fragment 2 is created (not necessarily made visible);
- lines 10–11: the view for fragment 1 is created (not necessarily made visible);
- lines 12–13: [onResume] method of fragment 1;
- lines 14-15: fragment 2's [onResume] method;
- line 16: activity menu is created;
Switch from tab 1 to tab 3:
06-03 02:50:02.685 2346-2346/examples.android D/MainActivity: getItem[2]
06-03 02:50:02.685 2346-2346/examples.android D/PlaceholderFragment: onCreateView 3
06-03 02:50:02.686 2346-2346/examples.android D/PlaceholderFragment: afterViews 3
06-03 02:50:02.686 2346-2346/examples.android D/PlaceholderFragment: onResume 3
06-03 02:50:02.686 2346-2346/examples.android D/PlaceholderFragment: onResume setText 3
06-03 02:50:03.024 2346-2346/examples.android D/PlaceholderFragment: onDestroyView 1
- line 1: the fragment container requests the third fragment;
- lines 2-3: the view for fragment 3 is created (not necessarily displayed);
- lines 4-5: fragment 3’s [onResume] method is executed;
- line 6: fragment 1’s [onDestroyView] method is executed. This means that when the user returns to fragment 1 or an adjacent fragment, this fragment’s lifecycle will be re-executed;
Returning from tab 3 to tab 1:
06-03 02:53:46.255 2346-2346/examples.android D/PlaceholderFragment: onCreateView 1
06-03 02:53:46.256 2346-2346/examples.android D/PlaceholderFragment: afterViews 1
06-03 02:53:46.256 2346-2346/examples.android D/PlaceholderFragment: onResume 1
06-03 02:53:46.256 2346-2346/examples.android D/PlaceholderFragment: onResume setText 1
06-03 02:53:46.604 2346-2346/examples.android D/PlaceholderFragment: onDestroyView 3
- Lines 1–4: Fragment 1’s lifecycle is re-executed because it had undergone an [onDestroyView];
- line 5: fragment 3’s [onDestroyView] method is now executed. Again, when the user returns to fragment 3 or an adjacent fragment, this fragment’s lifecycle will be re-executed;
1.8.7. setUserVisibleHint
The [onCreateView] method of the lifecycle instantiates the view associated with the fragment but does not necessarily make it visible. That is what we will see now. The [Fragment.setUserVisibleHint] method is executed every time the fragment’s visibility changes. We add this method to the fragment’s code:
package exemples.android;
....
// A fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// visual interface component
@ViewById(R.id.section_label)
protected TextView textViewInfo;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
}
}
At startup, the logs are as follows:
06-03 03:06:13.263 20586-20586/examples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/examples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/examples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/examples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/examples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/examples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/examples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/examples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/examples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/examples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/examples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/examples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/examples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/examples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/examples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/examples.android D/menu: creating menu
- The logs for lines 7, 9–10 show that only Fragment 1 becomes visible. We can also see that it becomes visible before its [onCreateView] method is executed;
Let’s switch from tab 1 to tab 2:
06-03 03:10:15.215 20586-20586/examples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/examples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/examples.android D/PlaceholderFragment: onResume setText 3
- Fragment 1 is hidden (line 3), Fragment 2 is displayed (line 4);
Let's switch from tab 2 to tab 3:
06-03 03:12:06.238 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/examples.android D/PlaceholderFragment: onDestroyView 1
- Fragment 2 is hidden (line 1), Fragment 3 is displayed (line 2);
Let's go back to tab 1:
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/examples.android D/PlaceholderFragment: onDestroyView 3
- Fragment 3 is hidden (line 2), Fragment 1 is displayed (line 3);
What have we learned?
- The [setUserVisibleHint] method is executed once with the [isVisibleToUser] property set to true for the fragment that is about to be displayed;
- We cannot determine when this method will be executed relative to the fragment’s lifecycle. Thus, for fragment 1, the [setUserVisibleHint, true] method was executed before the [onCreateView] method at the start of this fragment’s lifecycle, whereas for fragments 2 and 3, the opposite occurred;
1.8.8. setOffscreenPageLimit
The previous logs show that when the fragment container [ViewPager] is about to display fragment #i, it executes, if not already done, the lifecycle of the adjacent fragments i-1 and i+1. This behavior can be controlled by the [ViewPager].setOffscreenPageLimit method:
With the above instruction,
- when the fragment container [ViewPager] is about to display fragment #i, it executes, if it hasn’t already done so, the lifecycle of the adjacent fragments in the range [i-n, i+n];
- if fragment j is then displayed:
- the same phenomenon occurs for the adjacent fragments in the interval [j-n, j+n];
- the fragments initialized in step 1 that are no longer adjacent to the new fragment within the range [j-n, j+n] may then undergo an [onDestroyView] operation. However, I have observed in other applications, particularly the one in Chapter 3, that this was not always the case;
We modify the [MainActivity.afterViews] method as follows:
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
// toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// the fragment manager
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e., fragment #i in the fragment container is fragment #i provided by the fragment manager
mViewPager.setAdapter(mSectionsPagerAdapter);
// Disable swiping between fragments
mViewPager.setSwipeEnabled(false);
// Fragment offset
mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);
// the tab bar is also associated with the fragment container
// i.e., tab #i displays fragment #i from the container
tabLayout.setupWithViewPager(mViewPager);
// floating button
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
- Line 20: We set the number of adjacent fragments to initialize to the total number of fragments minus 1. Thus, at startup, when the fragment container displays fragment #1, it will simultaneously initialize fragments 2, 3, ..., n, where n = 1 + mSectionsPagerAdapter.getCount() - 1 = mSectionsPagerAdapter.getCount(). This means that all fragments will be initialized. When the viewport moves to another fragment, the fragment container:
- will detect that all fragments adjacent to the new fragment are already initialized and will therefore not initialize them;
- since the adjacency of the new fragment also covers all the fragments, none will be “deinitialized” by the fragment container;
In total, we should see all fragments instantiated and initialized when the application starts and then never again. This is what we’ll now verify by examining the logs.
At startup, we have the following logs:
- lines 4–6: construction of the three fragments;
- lines 7, 9, 11: the fragment container requests the three fragments. In the previous version, it requested two;
- lines 14-25: the lifecycle of the three fragments runs;
Now let’s switch from tab 1 to tab 2:
Let's switch from tab 2 to tab 3:
Then from tab 3 to tab 1:
The logs confirm the theory. All fragments were instantiated and initialized at startup. After that, their lifecycle methods are no longer executed. This is a very predictable behavior of fragments, which makes them much easier to use.
What we want to find is a way to update a fragment that is about to be displayed, regardless of the fragment adjacency chosen by the developer. The logs have shown us two things:
- the [setUserVisibleHint, true] method is always executed for the fragment that is about to be displayed, but not for the others;
- this event can occur before or after the fragment’s lifecycle. This depends on the fragment adjacency chosen by the developer. This is a problem because if the lifecycle has not yet occurred, it means the fragment cannot be updated by the [setUserVisibleHint, true] method;
The logs at application startup when the fragment adjacency was 1 were as follows:
06-03 03:06:13.263 20586-20586/examples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/examples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/examples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/examples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/examples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/examples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/examples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/examples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/examples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/examples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/examples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/examples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/examples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/examples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/examples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/examples.android D/menu: creating menu
- We can see that when Fragment 1 becomes visible, its view has not yet been created. Therefore, we cannot interact with it. This can be done during the fragment’s lifecycle, for example in the [onCreateView] method (line 11) or the [onResume] method (lines 13–14). Since we are using AA annotations, we normally do not need to write the [onCreateView] method. Therefore, the [onResume] method seems to be the most appropriate here for updating Fragment 1;
When we switched from tab 1 to tab 2, the logs were as follows:
06-03 03:10:15.215 20586-20586/examples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/examples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/examples.android D/PlaceholderFragment: onResume setText 3
This time, we only have the [setUserVisibleHint, true] method on line 4 to update fragment 2;
When we switched from tab 2 to tab 3, the logs were as follows:
06-03 03:12:06.238 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/examples.android D/PlaceholderFragment: onDestroyView 1
Here, we only have the [setUserVisibleHint, true] method on line 2 to update fragment 3;
When we switched from tab 3 to tab 1, the logs were as follows:
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/examples.android D/PlaceholderFragment: onDestroyView 3
Here, you must use the [onResume] method of Fragment 1 (lines 6–7) to update Fragment 1.
So in this example, we see that to update a fragment that is about to be displayed, we have two methods: [setUserVisibleHint] and [onResume].
We will implement this solution in a new project where each fragment must display the number of times it has been displayed, which we will call a visit. We will therefore need to update its display each time it is displayed. This is indeed the problem we are trying to solve.
Before that, let’s examine the final stage in the life cycle of an activity or fragment: when it is destroyed. The system may decide to destroy an activity if other activities with higher priority require resources that are currently unavailable. To free up these resources, the system will take the initiative to destroy certain activities. The [onDestroy] method of the activity and fragments will then be called.
1.8.9. OnDestroy
![]() | ![]() | ![]() |
We will allow the user to delete the activity using a menu option [5]. To do this, we add a new menu option to the [menu_main.xml] file [1]:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/action_terminate"
android:title="@string/action_terminate"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
Simply copy and paste the first menu option and adapt the result (lines 9 and 10). The label for this new option is added to the [strings.xml] file [2]:
<resources>
<string name="app_name">Example-07</string>
<string name="action_settings">Settings</string>
<string name="action_terminate">Terminate</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
Finally, in the [MainActivity] class, we handle the click on the [Terminate] option:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, as long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
if (id == R.id.action_terminate) {
Log.d("menu", "action_terminate selected");
//end the activity
finish();
return true;
}
// parent
return super.onOptionsItemSelected(item);
}
- lines 14–19: copy and paste lines 10–13 and adapt the code to the new option;
- line 17: the activity is terminated by a software action;
Now let’s run this new version, and as soon as the first view is displayed, click on the [Terminate] menu option. The logs are then as follows:
- lines 1-2: click on the [Terminate] option;
- line 4: the activity's [onDestroy] method is called;
- lines 4-5: fragment 1's [onDestroyView] method is called, followed by its [onDestroy] method;
- lines 6-9: this process repeats for the other two fragments;
It is important to remember that the [onDestroy] method of the activity and fragments is called when the activity is about to be destroyed by the system, the developer, or the user. This method can be used to save information—for example, locally on the tablet—so that it can be retrieved when the user restarts the application.
1.9. Example-08: Updating a fragment with variable fragment adjacency
1.9.1. Creating the project
Duplicate the [Example-07] project into [Example-08]. To do this, follow the procedure described for duplicating [Example-02] into [Example-03] in section 1.4.
![]() | ![]() |
1.9.2. Rewriting the [PlaceholderFragment] fragment
The new code for the [PlaceholderFragment] fragment is as follows. It works regardless of the adjacency assigned to the fragments (1, partial, total):
package exemples.android;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// A fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// visual interface component
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// data
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 0;
// fragment number
private static final String ARG_SECTION_NUMBER = "section_number";
// constructor
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
if (!initDone) {
// initial text
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// initialization complete
initDone = true;
}
// display current text
textViewInfo.setText(text);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
@Override
public void onDestroyView() {
...
}
@Override
public void onResume() {
...
}
// update fragment
public void update() {
// The task depends on the visit count
if (visitCount > 1) {
// log
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// updated text
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// local info for logs
private String getInfo() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s", numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone);
}
}
- lines 34-48: the [@AfterViews] method may be executed multiple times. We used to use it to initialize the fragment’s text (line 42). We still do this, but to ensure it happens only once, we manage a boolean [initDone] (line 44) to indicate that initialization has been completed and does not need to be repeated;
- lines 56–59: We introduce the [onDestroyView] method to account for the fact that the next time the fragment is re-displayed, its lifecycle will be re-executed;
- The logs showed that two methods can be executed after the [@AfterViews] method: the [setUserVisibleHint] and [onResume] methods. The [onResume] method is only executed when the fragment’s lifecycle is executed. The [setUserVisibleHint] method, however, is not always executed after the [@AfterViews] method. The logs showed that at least one of the two is executed after the [@AfterViews] method. The logs have never shown that both could be executed together after the [@AfterViews] method. It is either one or the other. As a precaution, we will set a boolean [updateDone] when an update has been made;
The [setUserVisibleHint] and [onResume] methods are as follows:
// data
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int visitCount = 0;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// memory
this.isVisibleToUser = isVisibleToUser;
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// number of visits
if (isVisibleToUser) {
// increment
numVisit++;
// update fragment
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// the fragment will be hidden
updateDone = false;
}
}
@Override
public void onResume() {
// parent
super.onResume();
// log
Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// update
if (isVisibleToUser && !updateDone) {
update();
updateDone = true;
}
}
- line 14: the fragment's visible status is stored;
- lines 22–25: if the fragment is visible and the [@AfterViews] method has been executed, the [update] method is executed and the boolean [updateDone] is set to true;
- lines 26–28: if the fragment is going to be hidden, the boolean [updateDone] is reset to false. We need an event to reset the [updateDone] boolean—which is set to true as soon as the [update] method is called—to false so that new updates can be made. We use the fact that the fragment is no longer visible to do this. When it becomes visible again, the fragment must be updated once more;
- lines 32–42: the logs show that depending on the adjacency chosen for the fragments, the [onResume] method may execute even though the fragment is not visible. If it is not visible, we do not perform the update (line 39) and, as we did for [setMenuVisibility], we manage the boolean [updateDone].
Finally, the [onDestroyView] method is as follows:
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
// update indicator
afterViewsDone = false;
// log
Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}
The [onDestroyView] method is executed when a fragment's lifecycle ends. Another lifecycle may resume later.
- Line 6: The [onDestroyView] method removes any connection to the view attached to the fragment. It will be recreated during the fragment’s next lifecycle. For now, we need to set the boolean [afterViews] to false to indicate that the connection to the view no longer exists;
We will run the application with 5 fragments having an adjacency of 2. The changes are made in [MainActivity]:
// number of fragments
private final int FRAGMENTS_COUNT = 5;
// fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT=2;
// the fragment manager
private SectionsPagerAdapter mSectionsPagerAdapter;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
....
// fragment offset
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
...
}
The startup logs are as follows:
- lines 8, 10, 12: the fragment container requests all fragments adjacent to fragment 1;
- lines 9, 11, 13: the [setUserVisibleHint] method of these fragments is executed with [visibleToUser] set to false;
- Line 14: The [setUserVisibleHint] method of fragment 1 is called with [visibleToUser] set to true;
- lines 15–17: the [afterViews] method of the 3 adjacent segments is called. Here we see a case where this method is called after a fragment has become visible (Fragment 1, line 14);
- lines 18–20: the [onResume] method of the 3 adjacent segments is called;
Switching from tab 1 to tab 2:
- because the fragment layout is shifted one position to the right, fragment 4 is claimed by the fragment container;
- line 2: the [setUserVisibleHint] method of fragment 4 is called with [visibleToUser] set to false;
- line 3: the [setUserVisibleHint] method of fragment 1 is called with [visibleToUser] set to false. As a result, fragment 1 is now hidden;
- line 4: the [setUserVisibleHint] method of fragment 2 is called with [visibleToUser] set to true. Fragment 2 is now visible;
- lines 5-6: the lifecycle of fragment 4 continues;
We switch from tab 2 to tab 3:
- because the fragment layout is shifted one position to the right, fragment 5 is claimed by the fragment container;
- line 2: the [setUserVisibleHint] method of fragment 5 is called with [visibleToUser] set to false;
- line 3: the [setUserVisibleHint] method of fragment 2 is called with [visibleToUser] set to false. As a result, fragment 2 is now hidden;
- line 4: the [setUserVisibleHint] method of fragment 3 is called with [visibleToUser] set to true. Fragment 3 is now visible;
- lines 5-6: the lifecycle of fragment 5 continues;
We switch from tab 3 to tab 4:
05-31 07:00:17.762 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 3: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:00:17.762 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:00:17.762 32551-32551/examples.android D/PlaceholderFragment: onDestroyView 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- Line 1: Fragment 3 is now hidden;
- line 2: fragment 4 is now visible. Note that fragment 4’s lifecycle is not executed. This was already done two steps earlier;
- line 3: fragment 1 leaves the vicinity of the displayed fragment 4. Its [onDestroyView] method is executed. The next time it is displayed, its view lifecycle [onCreateView, afterViews, onResume] will be re-executed;
We switch from tab 4 to tab 5:
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:04:19.004 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 5: numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:04:19.004 32551-32551/examples.android D/PlaceholderFragment: onDestroyView 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- Line 1: Fragment 4 is now hidden;
- line 2: fragment 5 is now visible. Note that the fragment 5 lifecycle is not executed. This was already done two steps earlier;
- line 3: fragment 2 leaves the vicinity of the displayed fragment 5. Its [onDestroyView] method is executed;
We switch from tab 5 to tab 1:
05-31 07:06:17.246 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 1: numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 2: numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:06:17.246 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/examples.android D/PlaceholderFragment: afterViews 1 numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/examples.android D/PlaceholderFragment: onResume 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/examples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/examples.android D/PlaceholderFragment: afterViews 2 numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/examples.android D/PlaceholderFragment: onResume 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/examples.android D/PlaceholderFragment: onDestroyView 4 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/examples.android D/PlaceholderFragment: onDestroyView 5 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- lines 1, 4, 5, 6: the fragment 1 lifecycle is re-executed. This is because it had lost its connection to its view;
- lines 2, 5, 8, 9: for the same reason, the fragment 2 lifecycle is re-executed;
- lines 10–11: fragments 4 and 5 are removed from the vicinity of the displayed fragment;
- line 7: fragment 1 is updated;
![]() |
The logs never showed that the [setUserVisibleHint] and [onResume] methods both attempted to update the fragment. It is either one or the other. The reader is invited to perform further tests and monitor the logs to fully understand the concepts of fragment adjacency and lifecycle.
Now, let’s set total adjacency and run the same tests.
In [MainActivity]:
// number of fragments
private final int FRAGMENTS_COUNT = 5;
// fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;
The startup logs are as follows:
05-31 07:34:44.717 28908-28908/examples.android D/MainActivity: constructor
05-31 07:34:44.844 28908-28908/examples.android D/MainActivity: afterViews
05-31 07:34:44.887 28908-28908/examples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/examples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/examples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/examples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/examples.android D/PlaceholderFragment: constructor
05-31 07:34:45.201 28908-28908/examples.android D/MainActivity: getItem[0]
05-31 07:34:45.201 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.201 28908-28908/examples.android D/MainActivity: getItem[1]
05-31 07:34:45.204 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/examples.android D/MainActivity: getItem[2]
05-31 07:34:45.204 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 3: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/examples.android D/MainActivity: getItem[3]
05-31 07:34:45.204 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/examples.android D/MainActivity: getItem[4]
05-31 07:34:45.205 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 5: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.207 28908-28908/examples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/examples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/examples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.209 28908-28908/examples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/examples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/examples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/examples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/examples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/examples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/examples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:46.548 28908-28908/examples.android D/menu: creating menu
- The logs show that the lifecycle of the 5 fragments is being executed;
- Fragment 1 is displayed on line 18;
Switching from tab 1 to tab 2:
- line 1: fragment 1 is hidden;
- line 2: fragment 2 is displayed;
Switching from tab 2 to tab 3:
- line 1: fragment 2 is hidden;
- line 2: fragment 3 is displayed;
Switching from tab 3 to tab 4:
- line 1: fragment 3 is hidden;
- line 2: fragment 4 is displayed;
Switching from tab 4 to tab 5:
- line 1: fragment 4 is hidden;
- line 2: fragment 5 is displayed;
We switch from tab 5 to tab 1:
05-31 07:42:22.549 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 5: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:42:22.549 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 1: numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:42:22.549 28908-28908/examples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- line 1: fragment 5 is hidden;
- line 2: fragment 1 is displayed;
- line 3: fragment 1 is updated;
Switching from tab 1 to tab 4:
05-31 07:44:13.129 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 1: numVisit=2, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:44:13.129 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:44:13.129 28908-28908/examples.android D/PlaceholderFragment: update 4: numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- line 1: fragment 1 is hidden;
- line 2: fragment 4 is displayed;
- line 3: fragment 4 is updated;
We can see that with full adjacency, the behavior of the fragments is much more predictable.
Now, let’s set the adjacency to zero and see what happens. The [MainActivity] class evolves as follows:
// number of fragments
private final int FRAGMENTS_COUNT = 5;
// fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = 0;
The startup logs are as follows:
06-01 03:11:52.068 5679-5679/examples.android D/MainActivity: constructor
06-01 03:11:52.353 5679-5679/examples.android D/MainActivity: afterViews
06-01 03:11:52.433 5679-5679/examples.android D/PlaceholderFragment: constructor
06-01 03:11:52.433 5679-5679/examples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/examples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/examples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/examples.android D/PlaceholderFragment: constructor
06-01 03:11:52.566 5679-5679/examples.android D/MainActivity: getItem[0]
06-01 03:11:52.566 5679-5679/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/examples.android D/MainActivity: getItem[1]
06-01 03:11:52.566 5679-5679/examples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.571 5679-5679/examples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/examples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/examples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
06-01 03:11:52.574 5679-5679/examples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
06-01 03:11:54.597 5679-5679/examples.android D/menu: creating menu
- In lines 8 and 10, we see that the fragment container has requested 2 fragments, numbers 1 and 2. Everything therefore proceeds as if there were an adjacency of 1. The adjacency of 0 has therefore been ignored.
1.9.3. Inter-fragment communication
In the previous architecture, we have one activity and n fragments. The user interacts with the various fragments. These interactions modify the application’s state. Here, the application’s state refers to the set of information it stores throughout its lifetime. The following problem then arises:
- when the user interacts with fragment i, the application transitions from state E1 to state E2;
- a user action on fragment i causes fragment j to be displayed;
- how do we update fragment j with the application’s current state E2?
From previous examples, we know how to update fragment j. But where do we find the application’s state E2 to update it?
There are different solutions to this problem. We have seen one: fragment i can pass the application’s state E2 to fragment j via arguments. We encountered this method in the [MainActivity] class when creating the fragments:
for (int i = 0; i < fragments.length; i++) {
// create a fragment
fragments[i] = new PlaceholderFragment_();
// we can pass arguments to the fragment
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
This solution isn’t immediately usable here. In fact, when the user clicks on tab j, which will display fragment j, our code isn’t called. Only system code is executed. We’ll see in a future project how to intercept a tab click, but for now we’ll take a different approach.
We’ve discussed the application’s state: the set of data managed by the application over time. Here, the application consists of an activity and n fragments, all instantiated once at application startup and whose lifetime matches that of the application. Therefore, any of these elements—or several of them together—can serve as candidates for storing the application’s state. Each fragment has access, via the [Fragment.getActivity()] method, to the activity that created it. Since all fragments have access to the activity, it seems natural to store the application state within it.
However, the result of the [Fragment.getActivity()] method depends on when it is called in the lifecycle. We illustrate this point by adding a few logs to the [PlaceholderFragment] class:
// update fragment
public void update() {
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// the work to be done depends on the visit number
if (numVisit > 1) {
// log
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// updated text
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// local info for logs
private String getInfo() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s, getActivity()==null:%s",
numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone, getActivity() == null);
}
- lines 14-16: the [getInfo] method displays part of the app's status;
We launch the app with a fragment adjacency of 2. The logs when the app starts:
06-01 03:26:13.769 10931-10931/exemples.android D/MainActivity: constructor
06-01 03:26:13.856 10931-10931/examples.android D/MainActivity: afterViews
06-01 03:26:13.864 10931-10931/examples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/examples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/examples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/examples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/examples.android D/PlaceholderFragment: constructor
06-01 03:26:14.535 10931-10931/examples.android D/MainActivity: getItem[0]
06-01 03:26:14.538 10931-10931/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/examples.android D/MainActivity: getItem[1]
06-01 03:26:14.538 10931-10931/examples.android D/PlaceholderFragment: setUserVisibleHint 2: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/examples.android D/MainActivity: getItem[2]
06-01 03:26:14.538 10931-10931/examples.android D/PlaceholderFragment: setUserVisibleHint 3: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.541 10931-10931/examples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.545 10931-10931/examples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/examples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/examples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/examples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/examples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/examples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:15.967 10931-10931/examples.android D/menu: creating menu
- lines 9, 10, 13, 14: we see that in the [setUserVisibleHint] methods, we have [getActivity()==null] if the fragment is not yet visible (isVisibleToUser==false);
- line 19: we see that when the execution flow reaches the [update] method of fragment 1, the [getActivity] method correctly returns the activity;
When fragment adjacency is set to 4 (full adjacency), the logs are as follows:
06-01 03:35:23.553 2814-2814/exemples.android D/MainActivity: constructor
06-01 03:35:23.751 2814-2819/exemples.android I/art: Ignoring second debugger -- accepting and dropping
06-01 03:35:23.900 2814-2814/examples.android D/MainActivity: afterViews
06-01 03:35:23.991 2814-2814/examples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/examples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/examples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/examples.android D/PlaceholderFragment: constructor
06-01 03:35:24.002 2814-2814/examples.android D/PlaceholderFragment: constructor
06-01 03:35:24.207 2814-2814/examples.android D/MainActivity: getItem[0]
06-01 03:35:24.207 2814-2814/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/examples.android D/MainActivity: getItem[1]
06-01 03:35:24.207 2814-2814/examples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/examples.android D/MainActivity: getItem[2]
06-01 03:35:24.207 2814-2814/examples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/examples.android D/MainActivity: getItem[3]
06-01 03:35:24.207 2814-2814/examples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/examples.android D/MainActivity: getItem[4]
06-01 03:35:24.207 2814-2814/examples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.210 2814-2814/examples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.211 2814-2814/examples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.214 2814-2814/examples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/examples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/examples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/examples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/examples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/examples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/examples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/examples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/examples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:26.602 2814-2814/examples.android D/menu: creating menu
We get the same results. We can conclude that as soon as the fragment is visible, the [getActivity] method returns the fragment’s activity. We also note that when execution reaches the [update] method of the fragment that is about to be displayed, the [getActivity] method does indeed return a value.
To illustrate inter-fragment communication, we are building a new project.
1.10. Example-09: Inter-fragment communication, swiping, and scrolling
1.10.1. Creating the project
We duplicate the [Example-07] project into [Example-08]. To do this, we will follow the procedure described for duplicating [Example-02] into [Example-03] in section 1.4.
![]() | ![]() |
1.10.2. The session
In this new project, we want the fragments to display the total number of fragments viewed by the user. Here, we need to maintain a counter that is accessible to all fragments. We will call the object that encapsulates the data shared by the fragments a "session." This terminology comes from web development, where data to be shared across different views requested by the same user is placed in a session. Encapsulating the information shared by the different fragments into a single object makes the code more readable.
The [Session] class will be as follows:
![]() |
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// number of fragments visited
private int numVisit;
// getters and setters
public int getNumVisit() {
return numVisit;
}
public void setNumVisit(int numVisit) {
this.numVisit = numVisit;
}
}
- line 8: the session will encapsulate the number of fragments visited;
- line 5: the [EBean] annotation is an AA annotation. The [scope] attribute specifies the scope (or lifetime) of the annotated class. Here, the [scope = EBean.Scope.Singleton] attribute makes the [Session] class a singleton: it will be instantiated once and only once when the application starts. A reference to a class annotated with [EBean] can then be injected into another class. This is the concept of dependency injection;
1.10.3. The [MainActivity]
The [MainActivity] activity evolves as follows:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
...
// session injection
@Bean(Session.class)
protected Session session;
// number of fragments
private final int FRAGMENTS_COUNT = 5;
// fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = 2;
@AfterInject
protected void afterInject(){
Log.d("MainActivity", "afterInject");
// session initialization
session.setNumVisit(0);
}
...
- lines 7-8: injection of the reference to the session singleton using the [@Bean] annotation. The annotation’s parameter is the class of the bean to be injected. The field annotated in this way cannot have [private] scope;
- line 15: the [@AfterInject] annotation is used to designate a method to be called once all injections for the class have been completed. Thus, when entering the [afterInject] method on line 16, the reference from line 8 has already been initialized;
- line 20: the visit counter is reset to zero;
1.10.4. The [PlaceholderFragment] fragment
The [PlaceholderFragment] fragment evolves as follows:
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
....
// session
protected Session session;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// memory
this.isVisibleToUser = isVisibleToUser;
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// number of visits
if (isVisibleToUser) {
// update fragment
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// The fragment will be hidden
updateDone = false;
}
}
// update fragment
public void update() {
// log
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// session
if (session == null) {
session = ((MainActivity) getActivity()).getSession();
}
// increment visit count
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
textViewInfo.setText(String.format("%s, visit %s", text, numVisit));
}
- line 7: the session;
- lines 35-37: we know that when we enter the [update] method, the [getActivity] method correctly returns the activity. We take this opportunity to retrieve the session and store it locally (line 36);
- lines 39–41: to increment the visit number, we retrieve it from the session. We could have placed this code in the [setUserVisibleHint] method starting from line 19, since we know that the [getActivity] method returns the activity at that point. Here, we decide not to assign a specific role to this method and to move the fragment-specific code into the fragment’s [update] method, which is designed for that purpose;
- line 43: displays the visit number;
When running this application with 5 fragments, with 2 fragments adjacent, the first logs are as follows:
05-31 08:38:47.305 20114-20114/exemples.android D/MainActivity: constructor
05-31 08:38:47.307 20114-20114/examples.android D/MainActivity: afterInject
05-31 08:38:47.351 20114-20114/examples.android D/MainActivity: afterViews
05-31 08:38:47.354 20114-20114/examples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/examples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/examples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/examples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/examples.android D/PlaceholderFragment: constructor
...
- Lines 2–3: We can see that the activity’s [afterInject] method is executed before its [afterViews] method;
Readers are invited to test this new application.
1.10.5. Disabling Swipe
In the previous app, when you swipe the Android emulator with the mouse to the left or right, the current view is replaced by the view on the right or left, as appropriate. This default behavior isn’t always desirable. We’ll learn how to disable view swiping.
Let’s return to the main XML view [activity_main]:
![]() |
In the view’s XML code, we find the fragment container:
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
Line 1 specifies the class that manages the activity's pages. This class is found in the [MainActivity] activity:
import android.support.v4.view.ViewPager;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// the fragment manager
private SectionsPagerAdapter mSectionsPagerAdapter;
// the fragment container
@ViewById(R.id.container)
protected ViewPager mViewPager;
...
Line 12: The fragment container is of type [android.support.v4.view.ViewPager] (line 1). To disable swiping, we need to extend this class as follows:
![]() |
package exemples.android;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// controls swiping
private boolean isSwipeEnabled;
// 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;
}
}
// setter
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
}
- line 8: the [MyPager] class extends the Android [ViewPager] class (line 4);
- when swiping with the finger, the event handlers on lines 24 and 34 can be called. Both return a boolean. They simply need to return the boolean [false] to disable swiping;
- line 11: the boolean used to indicate whether or not to allow the swipe gesture.
Once this is done, we must now use our new page manager. This is done in the XML view [activity_main.xml] and in the main activity [MainActivity]. In [activity_main.xml] we write:
![]() |
<exemples.android.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
Line 1: We use the new class. In [MainActivity], the code changes as follows:
package examples.android;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// the fragment manager
private SectionsPagerAdapter mSectionsPagerAdapter;
// the fragment container
@ViewById(R.id.container)
protected MyPager mViewPager;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
// the fragment container is associated with the fragment manager
// i.e., fragment #i in the fragment container is fragment #i provided by the fragment manager
mViewPager.setAdapter(mSectionsPagerAdapter);
// we disable swiping between fragments
mViewPager.setSwipeEnabled(false);
// the tab bar is also associated with the fragment container
...
- line 12: the page manager is now of type [MyPager];
- line 23: we enable or disable swiping.
Test this new version. Enable or disable swiping and observe the difference in how the views behave when you drag them to the right or left with the mouse. In all future applications, swiping will be disabled. We won’t mention it again.
1.10.6. Disable scrolling between fragments
Let’s continue with an improvement to the tab manager. When switching from tab 1 to tab 4, you see the two intermediate tabs, 2 and 3, scroll by. In Android jargon, this is called smoothScrolling. This behavior can become annoying if there are many tabs. It can be disabled by adding the following code to the fragment manager [MyPager]:
// controls swiping
private boolean isSwipeEnabled;
// controls scrolling
private boolean isScrollingEnabled;
...
// scrolling
@Override
public void setCurrentItem(int position) {
super.setCurrentItem(position, isScrollingEnabled);
}
// setters
...
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
Because the tab manager has been associated with the fragment manager [MyPager], when tab #i is clicked, fragment #i is displayed by the fragment container using the [setCurrentItem] method above (line 9). [position] is the number of the fragment to display;
- line 10: the [setCurrentItem] method of the parent class is called. The second argument set to [false] requests an immediate transition between the old and new fragments (no scrolling); set to [true] requests a transition via scrolling. Here, the second argument is the value of the field on line 4, a field that the developer can set using the method on lines 16–18;
If you want to disable scrolling, the [MainActivity] class will look like this:
...
// fragment offset
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// disable swiping between fragments
mViewPager.setSwipeEnabled(false);
// no scrolling
mViewPager.setScrollingEnabled(false);
...
Run the project again and verify that there is no longer any scrolling between tabs 1 and 4, for example. From here on, we will always disable scrolling. We won’t revisit this.
1.10.7. A new fragment
In our example, all fragments are of the same type [PlaceHolderFragment]. We will now learn how to create a new fragment and display it.
First, copy the view [vue1.xml] from the [Example-04] project into the [Example-09] project [1]:
![]() | ![]() |
- in [1], the view [vue1.xml];
- in [3], the view displays errors due to missing text in the [res/values/strings.xml] file;
In [2], we add the missing text by taking it from the [res/values/strings.xml] file in the [Example-04] project
<resources>
<string name="app_name">Example-07</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- view 1 -->
<string name="view1_title">View #1</string>
<string name="txt_nom">What is your name?</string>
<string name="btn_submit">Submit</string>
<string name="btn_view2">View 2</string>
</resources>
- Above, we added lines 6–9;
Now, we create the [Vue1Fragment] class, which will be the fragment responsible for displaying the [vue1.xml] view:
![]() |
The [Vue1Fragment] class will be as follows:
package exemples.android;
import android.support.v4.app.Fragment;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends Fragment {
// UI elements
@ViewById(R.id.editTextName)
protected EditText editTextName;
// event handler
@Click(R.id.buttonValider)
protected void validate() {
// display the entered name
Toast.makeText(getActivity(), String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- line 10: the [@EFragment] annotation ensures that the fragment used by the activity will actually be the [Vue1Fragment_] class. Keep this in mind. The fragment is associated with the [vue1.xml] view;
- lines 14–15: the component identified by [R.id.editTextNom] is injected into the [editTextNom] field on line 15;
- lines 18–20: the [doValider] method handles the 'click' event on the button identified by [R.id.buttonValider];
- line 21: the first parameter of [Toast.makeText] is of type [Activity]. The method [Fragment.getActivity()] retrieves the activity in which the fragment is located. This is [MainActivity] since, in this architecture, we have only one activity that displays different views or fragments;
In the [MainActivity] class, the fragment manager is implemented as follows:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// the fragments
private Fragment[] fragments;
// fragment number
private static final String ARG_SECTION_NUMBER = "section_number";
// constructor
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
// initialize the fragment array
fragments = new Fragment[FRAGMENTS_COUNT];
for (int 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[fragments.length - 1] = new Vue1Fragment_();
}
...
}
- line 13: there are [FRAGMENTS_COUNT] fragments: [FRAGMENTS_COUNT-1] fragments of type [PlaceholderFragment] (lines 14-21) and one fragment of type [Vue1Fragment_], line 23 (note the underscore);
Compile and then run the [Example-09] project. Tab 5 should look different:
![]() |
1.10.8. Derive all fragments from the same abstract class
The new [Vue1Fragment] fragment also needs to update itself when it is displayed. To do this, we will need to create code similar to that created for the [PlaceholderFragment] fragment. To avoid repetition, we will factor out what can be into an abstract class from which all fragments in the application will inherit.
To do this, we create a new project.
1.11. Example-10: Deriving all fragments from an abstract class
1.11.1. Creating the project
We duplicate the [Example-09] project into [Example-10]:
![]() | ![]() |
1.11.2. Debug mode management
We add the option to the project to show or hide debug mode logs. To do this, we add a static constant to the [MainActivity] class:
// debug mode
public static final boolean IS_DEBUG_ENABLED = false;
1.11.3. The abstract parent class of all fragments
![]() |
The [AbstractFragment] class is as follows:
package exemples.android;
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
public abstract class AbstractFragment extends Fragment {
// private data
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private String className;
// Data accessible to child classes
protected boolean afterViewsDone = false;
protected boolean isDebugEnabled = true;
// activity
protected MainActivity activity;
// session
protected Session session;
// constructor
public AbstractFragment() {
// init
isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
className = getClass().getSimpleName();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
...
}
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
...
}
@Override
public void onResume() {
// parent
super.onResume();
...
}
// local info
protected String getParentInfo() {
return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
}
// update fragment
protected void update() {
...
// ask the child class to update itself
updateFragment();
}
protected abstract void updateFragment();
}
- line 7: the [AbstractFragment] class extends the Android [Fragment] class;
- Every fragment must be able to update itself. That is why the parent class [AbstractFragment] requires its child classes to have an [updateFragment] method (line 68), which it calls (line 65);
- line 19: the class will store a reference to the application’s activity;
- line 22: the class will store a reference to the session where the data shared by the fragments and the activity is collected;
- lines 25–33: the constructor of the abstract class;
- line 27: creation of a copy of the constant [MainActivity.IS_DEBUG_ENABLED] in the field on line 16;
- line 28: the name of the instantiated class is stored, i.e., the name of a child class;
- lines 15–22: these fields have the [protected] attribute so that child classes can access them. Note that the child classes are unaware of the existence of the booleans [isVisibleToUser] and [updateDone] (lines 10–11);
- line 57: the [getParentInfos] method has the [protected] attribute so that child classes can call it;
The methods [setUserVisibleHint, onDestroyView, onResume] remain the same as they were in the [PlaceholderFragment] class from the previous project:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// local
this.isVisibleToUser = isVisibleToUser;
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
}
// when the fragment becomes visible
if (isVisibleToUser) {
// update fragment
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// exit the fragment
updateDone = false;
}
}
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
// update indicator
afterViewsDone = false;
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onDestroyView: %s", getParentInfos()));
}
}
@Override
public void onResume() {
// parent
super.onResume();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
}
if (isVisibleToUser) {
// update
if (!updateDone) {
update();
updateDone = true;
}
}
}
The [update] method is as follows:
// update fragment
protected void update() {
// retrieve the activity and session
if (activity == null) {
Activity activity = getActivity();
if (activity != null) {
this.activity = (MainActivity) activity;
this.session = this.activity.getSession();
}
}
// we ask the child class to update itself
updateFragment();
}
According to the code above, when a fragment’s [update] method is executed, the fragment is visible. This is important because it means that the [Fragment.getActivity] method then returns a reference to the application’s activity (see section 1.10.8), which in turn provides access to the session.
- lines 4–10: initialize the activity and session if they haven’t already been initialized;
- line 12: the [updateFragment] method of the child class is called. When this method executes, the [activity] and [session] fields to which it has access have already been initialized;
1.11.4. The [PlaceholderFragment] class
![]() |
The [PlaceholderFragment] class is structured as follows:
package exemples.android;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;
// A fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {
// visual interface component
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// data
private boolean initDone;
// data
private String text;
private int visitCount;
// fragment number
private static final String ARG_SECTION_NUMBER = "section_number";
// constructor
public PlaceholderFragment() {
super();
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", "constructor");
}
}
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
...
}
// update fragment
public void updateFragment() {
...
}
}
- Line 10: The [PlaceholderFragment] class extends the [AbstractFragment] class. With this architecture, writing a fragment involves:
- writing the [@AfterViews] method, which is used to initialize the fragment during its first lifecycle or to reset it if an [onDestroyView] has occurred previously. Line 39 is required to properly manage the fragment’s lifecycle;
- writing the [updateFragment] method, which updates the fragment just before it is displayed. This method can use the session of its parent class;
- writing the fragment’s event handlers. This is what we will do in future projects;
The [@AfterViews] and [updateFragment] methods remain the same as they were in the previous project:
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
if (!initDone) {
// initial text
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// init done
initDone = true;
}
// display current text
textViewInfo.setText(text);
}
// update fragment
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
// increment visit count
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
textViewInfo.setText(String.format("%s, visit %s", text, numVisit));
}
// local info for logs
protected String getLocalInfo() {
return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
numVisit, initDone, getActivity() == null);
}
- Lines 7 and 23: In the logs, we display information from the parent class using the inherited method [getParentInfos];
1.11.5. The [Vue1Fragment] class
![]() |
The [Vue1Fragment] class has the same structure as the [PlaceholderFragment] class:
package exemples.android;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// UI elements
@ViewById(R.id.editTextNom)
protected EditText editTextName;
// data
private int visitCount;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("View1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
}
}
// event handler
@Click(R.id.buttonValider)
protected void doValidate() {
// display the entered name
Toast.makeText(getActivity(), String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// local information for logs
protected String getLocalInfo() {
return String.format("numVisit=%s", numVisit);
}
// update fragment
@Override
protected void updateFragment() {
// increment visit count
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// display the visit count
Toast.makeText(getActivity(), String.format("Visit #%s", numVisit), Toast.LENGTH_SHORT).show();
}
}
- Line 9: The [Vue1Fragment] class extends the [AbstractFragment] class;
- lines 18–26: The [@AfterViews] method has nothing of interest to do. It must still be written to set the boolean [afterViewsDone] to true, as this information is used by the parent class;
- lines 42–49: the [updateFragment] method consists of displaying a short message showing the visit number (line 48) and incrementing this number in the session (lines 44–46);
Readers are invited to test this new project.
We will use this architecture in all future projects:
- one activity and n fragments;
- all fragments extend the [AbstractFragment] class;
- data to be shared between fragments and between fragments and the activity is placed in the [Session] class;
1.11.6. Tab/Fragment Association
In the [MainActivity] class, which manages the tabs, the following is written:
// the tab bar is also associated with the fragment container
// i.e., tab #i displays fragment #i from the container
tabLayout.setupWithViewPager(mViewPager);
Line 3 associates the tab manager with the fragment container. We have seen one consequence of this association: when the user clicks on tab #i, the fragment container displays fragment #i. We have not seen the reverse: when we ask the fragment container to display fragment #i, tab #i is automatically selected.
To illustrate this behavior, we will add the options [Fragment 1, Fragment 2, ...] to the current menu. When the user clicks the [Fragment i] option, we will ask the fragment container to display fragment #i. We will then see whether tab #i has been selected or not.
This step begins with modifying the application menu:
![]() | ![]() |
The contents of the file [res/menu/menu_main.xml] change 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="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"/>
<item android:id="@+id/fragment5"
android:title="@string/fragment5"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- lines 9–28: the five new menu options;
- the option labels (lines 10, 14, 18, 22, 26) are defined in the file [res/values/strings.xml] [2]:
<resources>
<string name="app_name">Example-10</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- view 1 -->
<string name="view1_title">View #1</string>
<string name="txt_name">What is your name?</string>
<string name="btn_submit">Submit</string>
<string name="btn_view2">View 2</string>
<!-- menu -->
<string name="fragment1">Fragment 1</string>
<string name="fragment2">Fragment 2</string>
<string name="fragment3">Fragment 3</string>
<string name="fragment4">Fragment 4</string>
<string name="fragment5">Fragment 5</string>
</resources>
The visual result is as follows:
![]() |
Click handling for these menu options is handled in the [MainActivity] class:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// process menu options
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d("menu", "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
case R.id.fragment5: {
showFragment(4);
break;
}
}
// item processed
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// change the displayed fragment
mViewPager.setCurrentItem(i);
}
}
- Line 2: The [onOptionsItemSelected] method is called when one of the menu options is clicked;
- line 8: we retrieve the ID of the clicked option;
- lines 9–36: the different cases are handled by a switch statement;
- lines 16–36: clicking the [Fragment i] option calls the [showFragment(i-1)] method in lines 41–45;
- line 43: the fragment container is asked to display the requested fragment;
- line 42: we first verify that this is possible (condition 1) and that it is necessary (condition 2);
Readers are invited to test this new version. We observe that when we request the display of fragment #i, it is indeed displayed and tab #i is itself selected.
Now that we have seen how the tab/fragment association works, we will look at another case: one where tab management is decoupled from fragment management. This is the case, for example, when there are fewer tabs than fragments. To illustrate this new use case, we will build a new project.
1.12. Example-11: Tabs Separated from Fragments
1.12.1. Creating the project
We duplicate the [Example-10] project into [Example-11]:
![]() | ![]() |
1.12.2. Objectives
The new application will have two tabs:
- The first tab will always display the fragment [View1];
- the second tab will display a fragment selected from the menu;

- in [1], the [View1] fragment;
- in [2], the [PlaceholderFragment] fragment selected by the user;
- in [3], visits continue to be counted;
1.12.3. The session
![]() |
The new session will be as follows:
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// number of fragments visited
private int numVisit;
// number of [PlaceholderFragment] fragments displayed in the second tab
private int numFragment;
// getters and setters
...
}
- Line 10: We will handle tab clicks ourselves. When a tab is clicked, we need to load the fragment that was displayed the last time it was selected. The [numFragment] field will store the fragment number for tab #2, a number in the range [0, Fragments_COUNT-2]. When tab #2 is clicked, we will retrieve the fragment number to display from the session;
1.12.4. The menu
![]() |
The menu [res / menu / menu_main.xml] changes 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="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>
Tab #2 will display one of the four fragments from lines 9–24. The fifth fragment is the [Vue1Fragment] fragment, which will always be displayed in Tab #1.
1.12.5. The [MainActivity] class
The [MainActivity] class must now manage the tabs and navigation between them, which it did not do previously. Its code changes as follows:
// the tab manager
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
...
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
...
// no scrolling
mViewPager.setScrollingEnabled(false);
// display View1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
// initially, there is only one tab
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("View 1");
tabLayout.addTab(tab);
// event handler
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// A tab has been selected - change the fragment displayed by the fragment container
...
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
...
}
- line 17: The first fragment displayed by the fragment container will be the [Vue1Fragment] fragment. By design, this will be the last fragment in the container;
- lines 20–22: because we haven’t established an association between tabs and the fragment container, we have to manage the tabs ourselves. Initially, the tab bar [tabLayout] on line 3 has no tabs;
- line 20: we create the first tab;
- line 21: we give it a title. In the previous examples, the tab titles were the same as the fragment titles. That is no longer the case. As a result, we remove the [getPageTitle] method from the fragment manager. We no longer need it:
// optional - gives a title to managed fragments
@Override
public CharSequence getPageTitle(int position) {
return String.format("Tab #%s", (position + 1));
}
- line 22: the created tab is added to the tab bar. Our tab bar now has a tab. What does this tab display? It is important to understand that tabs and fragments are two separate concepts. The fragment displayed is always the one chosen by the fragment container. If you switch tabs and do not ask the container to change the displayed fragment, nothing happens: the same fragment is still displayed, but the selected tab has changed. So here, the displayed fragment is the one chosen line 17: the fragment [Vue1Fragment];
- lines 26–30: the method to write to handle the user’s tab change;
The [onTabSelected] method in lines 26–30 is triggered whenever there is a tab change (if the user clicks on a tab that is already selected, nothing happens). Its code is as follows:
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (IS_DEBUG_ENABLED) {
Log.d("tabs", "onTabSelected");
}
// A tab has been selected - change the fragment displayed by the fragment container
// tab position
int position = tab.getPosition();
// number of the fragment to display
int fragmentNumber;
switch (position) {
case 0:
// fragment number [View1Fragment]
numFragment = FRAGMENTS_COUNT - 1;
break;
default:
// fragment number [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// display fragment
mViewPager.setCurrentItem(numFragment);
}
- line 8: we retrieve the position of the tab that was clicked. Here, we will get a number 0 or 1;
- lines 12–15: if the first tab was clicked, we prepare to display the fragment [Vue1Fragment];
- lines 16–18: in other cases (tab #2 clicked), we prepare to redisplay the fragment that was displayed the last time tab #2 was selected. Its ID was then stored in the app’s session;
- line 21: we ask the fragment container to display the desired fragment;
Now let’s look at managing the menu options (still in [MainActivity]):
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// process menu options
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d("menu", "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
}
// item processed
return true;
}
- lines 16–31: handling the 4 menu options. Each handler calls the [showFragment] method with the fragment number to display;
The [showFragment] method is as follows:
// tab #2
private TabLayout.Tab tab2 = null;
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// if the second tab does not yet exist, create it
if (tab2 == null) {
tab2 = tabLayout.newTab();
tabLayout.addTab(tab2);
}
// Set the title of the second tab
tab2.setText(String.format("Fragment #%s", (i + 1)));
// change the displayed fragment
mViewPager.setCurrentItem(i);
// the number of the displayed fragment is saved in the session
session.setNumFragment(i);
// Select tab 2—do nothing if it is already selected
tab2.select();
}
}
- Remember that when the application starts, there is only one tab;
- line 2: a reference to tab #2, initially null;
- line 5: the display conditions have not changed from the previous version;
- lines 7–10: if tab #2 does not yet exist, it is created (line 8) and added to the tab bar (line 9);
- line 12: the number of the fragment to be displayed is placed in the title of the second tab, with numbering starting at 1;
- line 14: the desired fragment is displayed;
- line 16: its number is stored in the session;
- line 18: tab #2 is selected. If it was already selected, nothing will happen: the [onTabSelected] method will not be executed. If it was not already selected, the [onTabSelected] method will be triggered. This method then instructs the fragment container to display the fragment already displayed in line 14. A simple check in the [onTabSelected] method prevents this scenario:
// display fragment only if necessary
if (numFragment != mViewPager.getCurrentItem()) {
mViewPager.setCurrentItem(numFragment);
}
Readers are invited to test this new version.
1.12.6. Improvements
We now have a solid understanding of fragments, their lifecycle, the concept of fragment adjacency, and their relationship with the tab bar. We also have a robust architecture that has just passed the test in Example 11:
- one activity and n fragments;
- all fragments extend the [AbstractFragment] class;
- data to be shared between fragments and between fragments and the activity is placed in the [Session] class;
In a new project, we will clarify the relationships between the activity and fragments by adding an interface.
1.13. Example 12: Defining the relationships between the activity and fragments
In this example, we want to define the minimal relationships between the activity and fragments. To do this, we will use:
- an interface [IMainActivity] that defines what fragments can request from the activity;
- an abstract class [AbstractFragment] that will define the state and methods that every fragment should have;
1.13.1. Creating the project
We duplicate the [Example-11] project into [Example-12] by following the procedure in section 1.4. We obtain the following result:
![]() | ![]() |
1.13.2. The [IMainActivity] interface
From the previous examples, it is clear that fragments need access to the session instantiated by the activity. Furthermore, although not visible in these examples, it is to be expected that event handlers in fragment s sometimes result in a view change. The activity will be asked to perform this change. The [IMainActivity] interface could then look like this:
![]() |
package exemples.android;
public interface IMainActivity {
// access to the session
Session getSession();
// view change
void navigateToView(int position);
// debug mode
boolean IS_DEBUG_ENABLED = true;
}
Line 12: Note the presence of a constant that was previously in the [MainActivity] class. We want to reduce the coupling between the fragments and the activity and limit it to a coupling between [AbstractFragment] and [IMainActivity]. The activity can then be named something other than [MainActivity]. Since the constant [IS_DEBUG_ENABLED] is used in the fragments, it is moved to the [IMainActivity] interface.
1.13.3. The abstract class [AbstractFragment]
The abstract class [AbstractFragment] changes very little:
// data accessible to child classes
protected boolean afterViewsDone = false;
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// activity
protected IMainActivity mainActivity;
protected Activity activity;
...
// update fragment
protected void update() {
// retrieve the activity and session
if (mainActivity == null) {
this.activity = getActivity();
if (this.activity != null) {
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
}
}
// we ask the child class to update itself
updateFragment();
}
- Lines 6 and 7: we maintain two types of references to the activity:
- line 6: a reference to the activity implementing the [IMainActivity] interface;
- line 7: a reference to the activity inheriting from the Android [Activity] class. This is the case for all activities;
These two references naturally point to the same object. However, this object is viewed as two different types. This will prevent type casting at runtime;
- line 14: we retrieve a reference to the activity using the [getActivity] method;
- line 15: if this reference is not null, then we can access the session;
- Lines 16–17: We store the activity as an implementation of the [IMainActivity] interface and the session;
1.13.4. Modifying the fragment manager
The fragment adapter [SectionsPagerAdapter] in the [MainActivity] class is modified in one place: instead of managing fragments of type [Fragment], it now manages fragments of type [AbstractFragment]:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// the fragments
private AbstractFragment[] fragments;
// fragment number
private static final String ARG_SECTION_NUMBER = "section_number";
// constructor
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
// initialize the fragment array
fragments = new AbstractFragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
...
}
// a fragment of +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
// fragment #position
@Override
public AbstractFragment getItem(int position) {
...
}
// returns the number of fragments managed
@Override
public int getCount() {
...
}
}
1.13.5. Modifying the [MainActivity] class
The [MainActivity] class must implement the [IMainActivity] interface:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{
...
// session injection
@Bean(Session.class)
protected Session session;
...
// session getter
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// display the view at position
if (mViewPager.getCurrentItem() != position) {
// display fragment
mViewPager.setCurrentItem(position);
}
}
- lines 10–12: the [getSession] method already existed;
- lines 15–22: the [navigateToView] method displays fragment #[position];
- line 17: we check if there is anything to do;
- line 19: fragment #[position] is displayed;
At this point, run the application. It should work.
1.13.6. Modifying the display of fragments in [MainActivity]
Currently, the [MainActivity] class displays a fragment using the statement:
// display View1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
Since the [navigateToView] method does the same thing, replace this type of statement everywhere (2 locations) with:
Then run the app. It should still work.
1.13.7. Conclusion
From now on, we will always use the previous architecture:
- an activity implementing the [IMainActivity] interface;
- fragments extending the [AbstractFragment] class, which requires them to implement the [updateFragment] method. These must also have an [@AfterViews] method in which they set the boolean [afterViewsDone] to true;
- a session encapsulating the data to be shared between fragments and the activity;
1.14. Example-13: Example-05 with fragments
In the [Example-05] project, we introduced navigation between views. At that time, it involved navigation between activities: 1 view = 1 activity. Here, we propose having a single activity with multiple views of type [AbstractFragment].
1.14.1. Creating the project
We duplicate the previous project [Example-12] into [Example-13] by following the procedure in section 1.4. We obtain the following result:
![]() | ![]() |
1.14.2. Project structure
We will begin using packages to organize the code. For now, we can distinguish two distinct domains:
- activity management;
- fragment management;
We create two packages for them: [examples.android.activity] and [examples.android.fragments]:
![]() |
![]() | ![]() |
We do the same to create the [examples.android.fragments] package:
![]() | ![]() |
In [8], we create a third package called [architecture] in which we will place the entities [IMainActivity, AbstractFragment, Session, MyPager], which are the building blocks of our app’s architecture. This serves as a reminder that we have made a specific architectural choice. Next, move the existing project elements as shown in [9]. Each move must be confirmed by clicking the [Refactor] button.
At this point, compile the application. We have the following errors in [MainActivity]:
![]() |
When moving classes to packages, Android Studio made the necessary changes to the application code (lines 18–21, for example). The classes referenced in lines 15 and 17 were not moved. They are generated by the Android Annotations library. For these classes, you must change the imports manually. These lines therefore become:
![]() |
Once this is done, there are no more compilation errors. Run the application. You will then see the following error:
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{exemples.android/exemples.android.MainActivity_}:
This error stems from the application manifest:
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="examples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Lines 3 and 12 specify that the designated activity is [examples.android.MainActivity_]. However, since the activity has been moved to the [activity] package, line 12 must now be:
android:name=".activity.MainActivity_"
Note the . before [activity]. Once again, Android Studio was unable to update the manifest because it references an Android Annotations class that has not been moved. Using the AA library therefore comes with a number of inconveniences.
1.14.3. Cleaning up the project
In the new project:
- there are no longer any tabs, floating buttons, or menus;
- the fragments [PlaceholderFragment] disappear. The app will manage two fragments: [Vue1Fragment], which we already have, and [Vue2Fragment], which we’ll need to create;
- the session is no longer the same;
1.14.3.1. Cleaning up the fragments
Delete the [PlaceHolderFragment] class [1]:
![]() | ![]() |
Similarly, delete the view [res/layout/fragment_main.xml] associated with this fragment [2].
1.14.3.2. Cleaning up the session
The session is currently as follows:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// number of fragments visited
private int numVisit;
// Fragment ID of type [PlaceholderFragment] displayed in the second tab
private int fragmentCount;
// getters and setters
public int getNumVisit() {
return numVisit;
}
public void setNumVisit(int numVisit) {
this.numVisit = numVisit;
}
public int getNumFragment() {
return numFragment;
}
public void setNumFragment(int numFragment) {
this.numFragment = numFragment;
}
}
We are not saving anything from this session.
Compile the project. The lines causing errors are those that used the session's content. Remove them. In the [Vue1Fragment] class, we also remove the [numVisit] variable from the code, which becomes the following:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// Visual interface elements
@ViewById(R.id.editTextName)
protected EditText editTextName;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("View1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// event handler
@Click(R.id.buttonValider)
protected void doValidate() {
// display the entered name
Toast.makeText(getActivity(), String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// update fragment
@Override
protected void updateFragment() {
}
}
1.14.3.3. Removing the tabs, floating button, and menu
Removing the tabs and the floating button is done in two places:
- in the view [res/layout/activity-main.xml], which defines these elements and their placement in the view;
- in the [MainActivity] activity code;
The menu is also removed in two places:
- in the [res/menu/menu-main.xml] view, which defines the menu options;
- in the [MainActivity] activity code;
The code for the [res / layout / activity-main.xml] view is currently 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.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<examples.android.architecture.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
- Remove lines [28-31, 41-47];
- also remove the toolbar from lines 18-24;
The menu code [res / menu / menu_main.xml] is currently 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/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>
- We will remove lines 9–24. This leaves an option that we will not use. Simply to provide an example of a menu option declaration that can be replicated via copy/paste;
In the [MainActivity] class, remove everything that refers to the tabs, the floating button, the toolbar, and the menu. The easiest way to find these references is to remove their declarations:
// the tab manager
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// the floating button
@ViewById(R.id.fab)
protected FloatingActionButton fab;
and recompile the app. The lines with errors are the ones that reference the missing elements. So delete all those lines. Also, modify the fragment manager so that it no longer references the [PlaceholderFragment] fragment that we deleted:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// the fragments
private AbstractFragment[] fragments;
// constructor
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
}
// fragment position
@Override
public AbstractFragment getItem(int position) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// returns the number of fragments managed
@Override
public int getCount() {
return fragments.length;
}
}
- Lines 7–10: We have removed all fragment generation;
At this point, there should no longer be any compilation errors. In the [MainActivity] class, we have arrived at the following intermediate code:
package exemples.android.activity;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import examples.android.R;
import examples.android.architecture.AbstractFragment;
import examples.android.architecture.IMainActivity;
import examples.android.architecture.MyPager;
import examples.android.architecture.Session;
import examples.android.fragments.View1Fragment_;
import org.androidannotations.annotations.*;
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
// the fragment container
@ViewById(R.id.container)
protected MyPager mViewPager;
// the toolbar
@ViewById(R.id.toolbar)
protected Toolbar toolbar;
// session injection
@Bean(Session.class)
protected Session session;
// number of fragments
private final int FRAGMENTS_COUNT = 5;
// fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = 2;
// debug mode
public static final boolean IS_DEBUG_ENABLED = true;
// the fragment manager
private SectionsPagerAdapter mSectionsPagerAdapter;
// constructor
public MainActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "constructor");
}
}
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
// toolbar - this is where the app name is displayed
setSupportActionBar(toolbar);
// 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 provided by the fragment manager
mViewPager.setAdapter(mSectionsPagerAdapter);
// fragment offset
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// Disable swiping between fragments
mViewPager.setSwipeEnabled(false);
// no scrolling
mViewPager.setScrollingEnabled(false);
// display View1
navigateToView(FRAGMENTS_COUNT - 1);
}
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
}
// session getter
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// display the view at position
if (mViewPager.getCurrentItem() != position) {
// display fragment
mViewPager.setCurrentItem(position);
}
}
// the fragment manager
// this is what we call to retrieve the fragments to display in the main view
// Must define the [getItem] and [getCount] methods—the others are optional
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// the fragments
private AbstractFragment[] fragments;
// constructor
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
}
// fragment position
@Override
public AbstractFragment getItem(int position) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// returns the number of managed fragments
@Override
public int getCount() {
return fragments.length;
}
}
}
There are a few more changes to make:
- delete line 31, which is no longer needed;
- line 33: set the fragment adjacency to 1;
- line 76: navigate to view 0. This will be the first one displayed;
- line 108: initialize the array with the fragment [Vue1Fragment_]:
// the fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
So we only have one fragment. Run the application. You should get the following result:

The [Validate] button should work.
1.14.4. Creating fragments and associated views
The application will have two views, those from the [Example-05] project. We already have the [vue1.xml] view in the current project. We will now duplicate [vue2.xml] from [Example-05] to [Example-12] (open both projects and copy and paste between them).
![]() | ![]() |
- In [1], the new view. When we try to edit it, errors appear [2]. We need to modify the [strings.xml] file [3] to add the strings referenced by this new view:
<resources>
<string name="app_name">Example-13</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- view 1 -->
<string name="view1_title">View 1</string>
<string name="txt_name">What is your name?</string>
<string name="btn_submit">Submit</string>
<!-- view 2 -->
<string name="btn_view2">View #2</string>
<string name="view2_title">View #2</string>
<string name="btn_view1">View #1</string>
</resources>
We duplicate the [View1Fragment] class into [View2Fragment]:
![]() |
and modify the copied code as follows:
package examples.android.fragments;
import android.util.Log;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("View2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// update fragment
@Override
protected void updateFragment() {
}
}
- line 9: the fragment is associated with the view [res/layout/view2.xml];
- line 10: the class extends the abstract class [AbstractFragment];
- lines 12–20: the required [@AfterViews] method;
- lines 23–25: the required [updateFragment] method;
1.14.5. Implementing fragments and navigation between them
The activity will now manage two fragments. Its [SectionsPagerAdapter] class is updated as follows:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// the fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
}
The [IMainActivity] interface handles navigation between views using its [navigateToView] method. We will handle the click on the [View 2] button in the [Vue1Fragment] fragment:
package examples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// Visual interface elements
@ViewById(R.id.editTextName)
protected EditText editTextName;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("View1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// event handlers ----------------------------------
@Click(R.id.buttonValider)
protected void doValidate() {
// display the entered name
Toast.makeText(activity, String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
mainActivity.navigateToView(1);
}
// update fragment
@Override
protected void updateFragment() {
}
}
- lines 37–40: the [showVue2] method handles the 'click' event on the [View #2] button;
- line 39: navigation is performed using the activity's [navigateToView] method. Recall that the activity was stored in the parent class as:
// activity
protected IMainActivity mainActivity;
and that this activity has already been initialized when entering any event handler.
- line 34: the statement uses the [activity] variable of the parent class, which is a reference to the activity as an instance of the Android [Activity] type;
protected Activity activity;
We find similar code for the [Vue2Fragment] fragment:
package examples.android.fragments;
import android.util.Log;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("View2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// event handlers ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showView1() {
mainActivity.navigateToView(0);
}
// Update fragment
@Override
protected void updateFragment() {
}
}
- Lines 24–27: The [showVue1] method handles the 'click' event on the [View 1] button;
Run the project and verify that navigation between views works.
1.14.6. Defining the session
The application works as follows:
- Enter a name in View 1;
- Display this name in View 2;
To allow View 1 to pass the entered name to View 2, we will use the following session;
package examples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// name
private String name;
// getters and setters
...
}
- line 8: the entered name;
The [MainActivity] class will initialize the session as follows:
// session injection
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// initialize session
session.setName("");
}
1.14.7. Final code for the fragments
In the [Vue1Fragment] fragment, we modify the code for the [Validate] button click handler:
package examples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// UI elements
@ViewById(R.id.editTextName)
protected EditText editTextName;
...
// event handlers ----------------------------------
@Click(R.id.buttonValider)
protected void validate() {
// store the entered name
String name = editTextName.getText().toString();
// display it
Toast.makeText(activity, name, Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showView2() {
// Store the entered name in the session
session.setName(editTextName.getText().toString());
// navigate to view #2
mainActivity.navigateToView(1);
}
// update fragment
@Override
protected void updateFragment() {
}
}
- lines: 31-37: handle the click on the [View #2] button;
- line 34: before navigating to View 2, we store the entered name in the session so that the new view can access it;
The [View2Fragment] view evolves as follows:
package exemples.android.fragments;
import android.util.Log;
import android.widget.TextView;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
// UI components
@ViewById(R.id.textViewHello)
protected TextView textViewHello;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("View2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// event handlers ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showView1() {
mainActivity.navigateToView(0);
}
// Update fragment
@Override
protected void updateFragment() {
// retrieve the name entered in the session
String name = session.getName();
// display it
textViewHello.setText(String.format("Hello %s!", name));
}
}
When view #2 is displayed, the name entered in view #1 must be displayed. We know that immediately after it is displayed, its [updateFragment] method will be executed. It is therefore in this method (lines 36–42) that we can place the code to display the name.
- lines 16–17: declaration of the view’s sole visual component;
- Line 39: The name entered in View 1 is retrieved from the session;
- Line 41: The label [textViewBonjour] is updated;
Run the project and verify that it works.
1.14.8. Managing the fragment lifecycle
In the [Vue1Fragment] fragment, the [@AfterViews] method is as follows:
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("View1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
This method is incomplete. In fact, we must always account for the case where the fragment is recycled after an [onDestroyView] operation. In this case, the view of Fragment 1 is regenerated, and any name that may have been entered previously will disappear from the view. We don’t want that. Currently, the entered name remains displayed because the adjacency of Fragment 1’s fragments means that the [Vue1Fragment] fragment’s lifecycle is executed only once. However, it is preferable to account for the fragment being recycled.
There are several ways to solve this problem:
- we can take advantage of the fact that the [update] method is executed systematically every time the fragment is displayed to update the entered name;
- you can perform this update only when the [@AfterViews] method is re-executed. We will take the latter approach;
We modify the code in [View1Fragment] as follows:
// UI elements
@ViewById(R.id.editTextNom)
protected EditText editTextName;
// data
private String name;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("View1Fragment", String.format("afterViews %s", getParentInfos()));
}
// (re)initialize the displayed text
editTextName.setText(name);
}
// event handlers ----------------------------------
...
@Click(R.id.buttonVue2)
protected void showView2() {
// we store the entered name so we can retrieve it if the fragment is recycled
name = editTextName.getText().toString();
// store the entered name in the session
session.setName(name);
// navigate to view #2
activity.navigateToView(1);
}
- line 27: as we are about to leave view 1 for view 2, we store the entered name;
- line 17: each time the fragment's lifecycle is executed, the last name entered is displayed again;
For the [View2Fragment] fragment, the existing code is sufficient:
// UI components
@ViewById(R.id.textViewHello)
protected TextView textViewHello;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("View2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// update fragment
@Override
protected void updateFragment() {
// retrieve the name entered in the session
String name = session.getName();
// display it
textViewHello.setText(String.format("Hello %s!", name));
}
- The only visual component of the view (line 3) is updated every time the view is displayed (line 21). The [@AfterViews] method therefore has nothing to add;
1.14.9. Conclusion
At this point, we have once again demonstrated the relevance of our architecture:
- an activity implementing the [IMainActivity] interface;
- fragments extending the [AbstractFragment] class, which requires them to implement the [updateFragment] method. These must also have an [@AfterViews] method in which they set the boolean [afterViewsDone] to true;
- a session encapsulating the data to be shared between fragments and the activity;
1.15. Example-14: A Two-Layer Architecture
We will build a single-view application with the following architecture:
![]() |
1.15.1. Creating the project
We duplicate the previous project [Example-12] into [Example-13] by following the procedure in section 1.4. We obtain the following result:
![]() | ![]() |
1.15.2. The view [view1]
The application will have only one view [view1.xml]. Therefore, we will delete the other view [view2.xml] along with its associated fragment:
![]() | ![]() |
Compile the application. Errors appear in [MainActivity]:
![]() |
Correct line 4 below in the fragment manager [SectionsPagerAdapter]
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// the fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
Line 4 above becomes:
// the fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
Remove the imports that are no longer needed [Ctrl-Shift-O]. There should no longer be any compilation errors. Run the project: view #1 should appear. We will now modify it.
We will create the view [vue1.xml] that will generate random numbers:
![]() |
Its components are as follows:
Its XML code is as follows:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="20dp"
android:orientation="vertical" >
<TextView
android:id="@+id/txt_Title2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/txt_nbaleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_Titre2"
android:layout_marginTop="20dp"
android:text="@string/txt_nbaleas" />
<EditText
android:id="@+id/edt_nbaleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_nbaleas"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_nbaleas"
android:inputType="number" />
<TextView
android:id="@+id/txt_errorNbAleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edt_nbaleas"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_nbaleas"
android:text="@string/txt_errorNbAleas"
android:textColor="@color/red" />
<TextView
android:id="@+id/txt_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_nbaleas"
android:layout_marginTop="20dp"
android:text="@string/txt_a" />
<EditText
android:id="@+id/edt_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_a"
android:inputType="number" />
<TextView
android:id="@+id/txt_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_a"
android:text="@string/txt_b" />
<EditText
android:id="@+id/edt_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_b"
android:inputType="number" />
<TextView
android:id="@+id/txt_errorInterval"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edt_b"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_b"
android:text="@string/txt_errorInterval"
android:textColor="@color/red" />
<Button
android:id="@+id/btn_Execute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_a"
android:layout_marginTop="20dp"
android:text="@string/btn_execute" />
<TextView
android:id="@+id/txt_Answers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btn_Execute"
android:layout_marginTop="30dp"
android:text="@string/list_reponses"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/blue" />
<ListView
android:id="@+id/lst_answers"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_Answers"
android:layout_marginTop="40dp"
android:background="@color/wheat"
android:clickable="true"
tools:listitem="@android:layout/simple_list_item_1" >
</ListView>
</RelativeLayout>
The previous view uses labels defined in the [res/values/strings.xml] file:
<resources>
<string name="app_name">Example-14</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- view 1 -->
<string name="view1_title">View 1</string>
<string name="list_answers">List of answers</string>
<string name="btn_execute">Execute</string>
<string name="random_numbers">Generate N random numbers</string>
<string name="txt_n_random">Value of N:</string>
<string name="txt_a">"Generation interval [a,b], a: "</string>
<string name="txt_b">"b: "</string>
<string name="txt_dummy">Dummy</string>
<string name="txt_errorNbRandom">Enter an integer >=1</string>
<string name="txt_errorInterval">The interval bounds must be integers and b ≥ a</string>
</resources>
The colors used in [vue1.xml] are defined in the file [res/values/colors.xml]:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<!-- app colors -->
<color name="red">#FF0000</color>
<color name="blue">#0000FF</color>
<color name="wheat">#FFEFD5</color>
<color name="floral_white">#FFFAF0</color>
</resources>
1.15.3. The session
![]() |
Since there is only one fragment here, there is no need to plan for inter-fragment communication. The session will therefore be empty:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
}
At this point, compile the application. Errors will appear on the lines that used elements from the now-empty session. Remove these lines and verify that the compilation no longer produces errors.
1.15.4. The [Vue1Fragment] fragment
![]() |
We modify the existing [Vue1Fragment] fragment as follows:
package exemples.android.fragments;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import examples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// Visual interface elements
@ViewById(R.id.lst_reponses)
protected ListView listAnswers;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNumberOfElements;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorRandom;
@ViewById(R.id.txt_errorInterval)
protected TextView txtErrorInterval;
// list of responses to a command
private List<String> responses = new ArrayList<>();
// ListView adapter
private ArrayAdapter<String> adapterResponses;
// user input
private int numberOfRandomNumbers;
private int a;
private int b;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("View1Fragment", String.format("afterViews %s", getParentInfos()));
}
// hide error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorInterval.setVisibility(View.INVISIBLE);
}
@Click(R.id.btn_Execute)
void doExecute() {
// hide any previous error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorInterval.setVisibility(View.INVISIBLE);
// Check the validity of the entries
if (!isPageValid()) {
return;
}
}
// Check the validity of the entered data
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("View1Fragment", String.format("updateFragment %s", getParentInfos()));
}
}
}
- There is only one fragment here whose lifecycle will be executed only once, at application startup. For this reason, the [@AfterViews] method (lines 46–57) and the [updateFragment] method (lines 75–81) will be executed only once at application startup;
- lines 55-56: we hide the two error messages from the view (shown below) [1-2];
![]() |
- lines 59-60: the method executed when the [Execute] button is clicked;
- lines 71-73: the validity of the entries is checked;
The [isPageValid] method is as follows:
// the inputs
private int nbRandom;
private int a;
private int b;
...
// Check the validity of the entered data
private boolean isPageValid() {
// Enter the number of random numbers
nbRandom = 0;
Boolean error;
int numberOfErrors = 0;
try {
nbRandom = Integer.parseInt(edtNbRandom.getText().toString());
error = (nbRandom = < 1);
} catch (Exception ex) {
error = true;
}
// error?
if (error) {
errorCount++;
txtErrorAleas.setVisibility(View.VISIBLE);
}
// Enter a
a = 0;
error = false;
try {
a = Integer.parseInt(edtA.getText().toString());
} catch (Exception ex) {
error = true;
}
// error?
if (error) {
errorCount++;
txtErrorInterval.setVisibility(View.VISIBLE);
}
// Set b
b = 0;
error = false;
try {
b = Integer.parseInt(edtB.getText().toString());
error = b < a;
} catch (Exception ex) {
error = true;
}
// error?
if (error) {
errorCount++;
txtErrorInterval.setVisibility(View.VISIBLE);
}
// return
return (nbErrors == 0);
}
- Lines 2–4: These three fields are initialized by the [isPageValid] method. Additionally, this method returns true if all entries are valid, and false otherwise. If any entries are invalid, the associated error messages are displayed;
At this point, the application is executable. Verify the functionality of the [isPageValid] method by entering incorrect data.
1.15.5. The [business] layer
![]() |
![]() |
The [business] layer provides the following [IMetier] interface:
package exemples.android.metier;
import java.util.List;
public interface IBusiness {
List<Object> getRandom(int a, int b, int n);
}
The method [getAleas(a,b,n)] normally returns n random integers in the range [a,b]. It is also designed to throw an exception one out of every three times, and this exception is included in the results returned by the method. Ultimately, the method returns a list of objects of type [Exception] or [Integer].
The [Metier] implementation of this interface is as follows:
package exemples.android.metier;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@EBean(scope = EBean.Scope.Singleton)
public class BusinessImplementation implements IBusinessImplementation {
public List<Object> getRandom(int a, int b, int n) {
// the list of objects
List<Object> responses = new ArrayList<Object>();
// some checks
if (n < 1) {
answers.add(new AleaException("The number of random integers requested must be greater than or equal to 1"));
}
if (a < 0) {
answers.add(new AleaException("The number a in the interval [a,b] must be greater than 0"));
}
if (b < 0) {
answers.add(new AleaException("The number b in the interval [a,b] must be greater than 0"));
}
if (a >= b) {
answers.add(new AleaException("In the interval [a,b], a must be less than b"));
}
// error?
if (answers.size() != 0) {
return answers;
}
// Generate random numbers
Random random = new Random();
for (int i = 0; i < n; i++) {
// generate a random exception 1 time out of 3
int number = random.nextInt(3);
if (number == 0) {
answers.add(new AleaException("Random exception"));
} else {
// otherwise, return a random number between two bounds [a, b]
answers.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
}
}
// result
return answers;
}
}
- Line 9: We use the AA annotation [@EBean] on the [Business] class so that we can inject references to it into the [Presentation] layer. The attribute (scope = EBean.Scope.Singleton) ensures that only a single instance of the [Business] class will be created. Therefore, the same reference is always injected if it is injected multiple times into the [Presentation] layer;
- the rest of the code is standard;
The [AleaException] type used by the [Metier] class is as follows:
package exemples.android.metier;
public class AleaException extends RuntimeException {
private static final long serialVersionUID = 1L;
public AleaException() {
}
public AleaException(String detailMessage) {
super(detailMessage);
}
public AleaException(Throwable throwable) {
super(throwable);
}
public AleaException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
}
- Line 3: The [AleaException] class extends the system class [RuntimeException], making it an unhandled exception: it does not need to be handled in a try/catch block, nor does it need to be included in method signatures;
1.15.6. The [MainActivity] revisited
![]() |
[Business] LayerActivityUserView
The activity will implement the [IMetier] interface of the [business] layer. Thus, the fragment/view will have only the activity as its counterpart.
The [MainActivity] activity already implements the [IMainActivity] interface. To have it also implement the [IMetier] interface, we can:
- add the [IMetier] interface to the interfaces implemented by the activity;
- ensure that the [IMainActivity] interface itself extends the [IMetier] interface. This is the approach we are taking;
The [IMainActivity] interface becomes the following:
![]() |
package exemples.android.architecture;
import exemples.android.metier.IMetier;
public interface IMainActivity extends IMetier {
// access to the session
Session getSession();
// change view
void navigateToView(int position);
// debug mode
public static final boolean IS_DEBUG_ENABLED = true;
}
- line 5: the [IMainActivity] interface extends the [IMetier] interface
The [MainActivity] class evolves as follows:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
...
// session injection
@Bean(Session.class)
protected Session session;
// business logic injection
@Bean(Metier.class)
protected IBusinessLogic businessLogic;
...
// IBusiness implementation --------------------------------------------------------------------
@Override
public List<Object> getAleas(int a, int b, int n) {
return business.getRandom(a, b, n);
}
- lines 11-12: the [business] layer is injected into the activity. To do this, we use the [@Bean] annotation, whose parameter is the class bearing the [@EBean] annotation;
- line 2: the activity implements the [IMainActivity] interface and therefore the [IMetier] interface of the [business] layer;
- lines 16–19: implementation of the single method of the [IMetier] interface. We simply delegate the call to the [business] layer;
1.15.7. The [Vue1Fragment] fragment revisited
![]() |
The code for the [Vue1Fragment] class evolves as follows:
package exemples.android.fragments;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import examples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// elements of the user interface
@ViewById(R.id.lst_reponses)
protected ListView listAnswers;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNumberOfEntries;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorRandom;
@ViewById(R.id.txt_errorInterval)
protected TextView txtErrorInterval;
// list of responses to a command
private List<String> answers = new ArrayList<>();
// ListView adapter
private ArrayAdapter<String> adapterAnswers;
// user input
private int numberOfRandomNumbers;
private int a;
private int b;
@AfterViews
protected void afterViews() {
...
}
@Click(R.id.btn_Execute)
void doExecute() {
...
}
// Check the validity of the entered data
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("View1Fragment", String.format("updateFragment %s", getParentInfos()));
}
// will only be executed once when the application starts
// create the ListView adapter—this requires that the [activity] variable has been initialized
adapterAnswers = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, answers);
listAnswers.setAdapter(answerAdapter);
}
}
- Lines 69-70: Set the adapter for the [ListView] component;
The [ListView] component is used to display a list of items. It does this using a [ListAdapter] adapter, which is itself connected to the data source that feeds the [ListView]. To define the adapter for a [ListView], use the following [ListView.setAdapter] method:
public void setAdapter (ListAdapter adapter)
[ListAdapter] is an interface. The [ArrayAdapter] class is a class that implements this interface. The constructor used in line 69 above is as follows:
- [context] is the activity that displays the [ListView];
- [resource] is the integer identifying the view used to display an item in the [ListView]. This view can be of any complexity. The developer constructs it according to their needs;
- [textViewResourceId] is the integer identifying a [TextView] component in the [resource] view. The displayed string will be shown by this component;
- [objects]: the list of objects displayed by the [ListView]. The [toString] method of the objects is used to display the object in the [TextView] identified by [textViewResourceId] within the view identified by [resource].
The developer’s task is to create the [resource] view that will display each item in the [ListView]. For the simple case where we only want to display a single string of characters, as here, Android provides the view identified by [android.R.layout.simple_list_item_1]. This contains a [TextView] component identified by [android.R.id.text1]. This is the method used on line 69 to create the [ListView] adapter. This adapter only needs to be defined once. To allow for its reuse, it has been defined as an instance variable of the class (line 39). Let’s look again at line 69:
adapterAnswers = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, answers);
The first parameter of the [ArrayAdapter] constructor is the activity obtained from a fragment via [getActivity] and stored here in the [activity] variable of the parent class. This field does not always have a value. Thus, the logs show that when we reach the [@AfterViews] method, it has not yet been initialized, so we cannot place lines 69–70 in this method. In the [updateFragment] method, this is possible because we know from that when this method is executed, [activity] is necessarily not null. The adapter is here associated with the [reponses] data source defined on line 37;
The [doExecute] method handles the click on the [Execute] button. Its code is as follows:
@Click(R.id.btn_Executer)
void doExecute() {
// we hide any previous error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorInterval.setVisibility(View.INVISIBLE);
// clear previous responses
responses.clear();
adapterAnswers.notifyDataSetChanged();
// Check the validity of the entries
if (!isPageValid()) {
return;
}
// request random numbers from the activity
List<Object> data = mainActivity.getRandom(a, b, nbRandom);
// create a list of Strings from this data
for (Object o : data) {
if (o instanceof Exception) {
answers.add(((Exception) o).getMessage());
} else {
answers.add(o.toString());
}
}
// refresh listview
adapterAnswers.notifyDataSetChanged();
}
- lines 7-8: we want to clear the ListView. To do this, we clear the data source [reponses] and ask the adapter associated with the ListView to refresh;
- lines 10-12: Before executing the requested action, we verify that the entered values are correct;
- line 14: the list of random numbers is requested from the activity. We obtain a list of objects where each object is of type [Integer] or [AleaException];
- lines 16–22: using the list of objects obtained, the [reponses] data source displayed by the ListView is updated;
- line 24: the ListView adapter is asked to refresh;
1.15.8. Execution
Run the project and verify that it works correctly.
1.16. Example-15: Client/server architecture
We’ll now look at a common architecture for an Android app, one where the Android app communicates with remote web services. We’ll now have the following architecture:
![]() |
We have added a [DAO] layer to the Android app to communicate with the remote server. It will communicate with the server that generates the random numbers displayed by the Android tablet. This server will have the following two-tier architecture:
![]() |
Clients query specific URLs in the [web/JSON] layer and receive a text response in JSON (JavaScript Object Notation) format. Here, our web service will handle a single URL of the form [/a/b], which will return a random number in the range [a,b]. We will describe the application in the following order:
The server
- its [business] layer;
- its [web/JSON] service implemented with Spring MVC;
The client
- its [DAO] layer. There will be no [business] layer;
1.16.1. The [web/JSON] server
We want to build the following architecture:
![]() |
1.16.1.1. Project creation
We will build the web service using the Spring ecosystem [http://spring.io/]. We go to the website [http://start.spring.io/] (June 2016), which will allow us to generate a Gradle project with the dependencies required for our project—which is not an Android project and for which Android Studio offers no assistance:
![]() |
- in [1]: choose a Gradle project;
- in [2-3]: the properties of the JAR dependency generated by the project (see below);
- in [4]: select the web dependency [5] so that the binaries required for our web service are available;
- in [6]: generate the project. A ZIP file of a Gradle skeleton project is then generated and made available for download;
What should you put in [2-3]? We have already used Gradle dependencies. For example, the one from the previous project was as follows:
![]() |
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
...
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
- Line 22: A dependency is specified in the format [groupId:artifactId:version]. What is requested on the form at [http://start.spring.io/]:
- in [2] is [groupId];
- in [3] is [artifactId];
Unzip the obtained zip file into the folder containing the other projects:
![]() | ![]() ![]() | ![]() |
Using Android Studio, open the Gradle project [server-01] [1-2]. The open project is in [3] (Project view).
1.16.1.2. Gradle Configuration
![]() |
The generated Gradle file (June 2016) is as follows:
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
eclipse {
classpath {
containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
}
}
- Lines 14 and 34–38 are for the Eclipse IDE. We remove them;
- Lines 1–11 and 15 are used to add a plugin called [spring-boot] to our Gradle project. Spring Boot is a project within the Spring ecosystem [http://projects.spring.io/spring-boot/]. This plugin defines the versions of the dependencies most commonly used with Spring. This allows us to omit specifying their versions (lines 30 and 31). The version is then the one defined by the Spring Boot version used (line 3);
- lines 22–23: the Java version to use, here version 1.8;
- lines 25–27: the binary repositories to use for downloading dependencies;
- line 26: specifies the Maven Central Repository. This is currently the largest open-source binary repository available;
- lines 29–32: the dependencies required for the project:
- line 30: this dependency includes all the binaries needed to build a Spring web service;
- line 31: this dependency includes all the binaries needed for testing, particularly JUnit tests;
- A [compile] dependency indicates that the dependency is needed to compile the project. A [testCompile] dependency indicates that the dependency is needed only for running tests. It is therefore not included in the project binary;
We’ll start by cleaning up the Gradle file:
// spring boot
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
// plugins
apply plugin: 'java'
apply plugin: 'spring-boot'
// project binary
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
// Java versions
sourceCompatibility = 1.8
targetCompatibility = 1.8
// Maven repositories
repositories {
mavenLocal()
mavenCentral()
}
// dependencies
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
- line 30: we have added the local Maven repository for the development machine. This is created when Maven is installed (see section 6.10). If the requested dependency is already in the local Maven repository, it will not be fetched from the central Maven repository;
- lines 19–22: a Gradle task to generate the project’s binary. We will use it to see what is being done;
![]() | ![]() | ![]() |
- In [1-4], run the [jar] task defined in the [build.gradle] file ([1] is located at the top right and on the side of the IDE);
The previous step creates the project's JAR archive and places it in the [build/libs] folder [5]:
![]() |
The archive name is derived directly from the information provided to the [jar] task in the [build.gradle] file (lines 19–22).
All of the project’s dependencies can be viewed as follows:
![]() |
We can see in [1] that the project’s single dependency [compile('org.springframework.boot:spring-boot-starter-web')] has brought along dozens of binaries. Spring Boot for the web has included the dependencies that a Spring MVC web application will likely need. This means that some may be unnecessary. Spring Boot is ideal for a tutorial:
- it includes the dependencies we’ll likely need;
- it includes an embedded Tomcat server [1], which saves us from having to deploy the application on an external web server;
You can find many examples using Spring Boot on the Spring ecosystem website [http://spring.io/guides].
We will now complete the [build.gradle] file as follows:
// spring boot
...
// dependencies
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
// plugin to create a Maven-compliant binary in the local Maven repository
apply plugin: 'maven-publish'
publishing {
publications {
maven(MavenPublication) {
groupId 'istia.st.examples.android'
artifactId 'server-01'
version '0.0.1-SNAPSHOT'
from components.java
}
}
repositories {
maven {
// change to point to your repository, e.g. http://my.org/repo
url 'file://D:\\maven'
}
}
}
- line 10: we import a Gradle plugin called [maven-publish] that allows us to publish the project's binary to a Maven repository in accordance with Maven standards;
- line 11: a Gradle task called [publishing];
- lines 14–15: the characteristics of the Maven binary that will be created;
- line 23: the Maven repository to which it will be published, in this case a local Maven repository;
Adding the [maven-publish] plugin has created new tasks in the Gradle project:
![]() | ![]() |
If, in [2], we run the [publish] task, the project binary is created and installed in the folder specified on line 23 of the [build.gradle] file:
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |
![]() |
The [jar] task generates the project’s binary. This binary does not include its dependencies and is therefore not executable. It is possible to generate an executable binary that includes all its dependencies. To do this, we add the following code to the [build.gradle] file:
// create a binary with all its dependencies
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'istia.st.exemples.android.Server01Application'
}
baseName = project.name + '-all'
from { configurations.compile.collect { if (it.isDirectory()) { it } else { zipTree(it) } }
with jar
}
- Line 6: Enter the full name of the project's executable class:
![]() |
The code for this class will be as follows:
package istia.st.exemples.android;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Server01Application {
public static void main(String[] args) {
System.out.println("Server01Application running");
//SpringApplication.run(Server01Application.class, args);
}
}
Refresh the Gradle project and then run the [fatJar] task:
![]() | ![]() |
The binary is generated in the [build/libs] folder and can be run [1-7]:
![]() | ![]() |
1.16.1.3. Project Configuration
The Gradle configuration is not enough. We also need to configure the project. Since this is not an Android project generated by the IDE, this configuration—which we haven’t done until now—must be done here.
![]() | ![]() |
- In [3-4]: use JDK 1.8;
To compile the project, the button available for Android projects is no longer present. We will use a menu option [1-2]:
![]() | ![]() |
Next, the reader is prompted to create the following project. We will comment on the final project code [3].
1.16.1.4. The [business] layer
![]() |
![]() |
The [business] layer follows the same approach as the [business] layer in the previous example. It will have the following [IMetier] interface:
package exemples.android.server.metier;
public interface IBusiness {
// random number in [a,b]
int getRandom(int a, int b);
}
- Line 5: the method that generates 1 random number in [a,b]
The code for the [Metier] class implementing this interface is as follows:
package exemples.android.server.metier;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Random;
@Service
public class BusinessImplementation implements IBusinessImplementation {
@Override
public int getRandom(int a, int b) {
// some checks
if (a < 0) {
throw new RandomException("The number a in the interval [a,b] must be greater than 0", 2);
}
if (b < 0) {
throw new AleaException("The number b in the interval [a, b] must be greater than 0", 3);
}
if (a >= b) {
throw new AleaException("In the interval [a,b], a must be less than b", 4);
}
// generate result
Random random = new Random();
random.setSeed(new Date().getTime());
return a + random.nextInt(b - a + 1);
}
}
We won’t comment on the class: it is similar to the one in the previous example, except that it does not throw exceptions randomly. Note the Spring annotation [@Service] on line 8, which causes Spring to instantiate the class as a single instance (singleton) and make its reference available to other Spring components. Other Spring annotations could have been used here to achieve the same effect. Spring components have default names that can be specified as an attribute of the annotation used. Without this attribute, as here, the Spring component takes the name of the class with its first character lowercase. Thus, here, the Spring component is named [metier] by default;
The [Metier] class throws exceptions of type [AleaException]:
package exemples.android.server.metier;
public class AleaException extends RuntimeException {
// error code
private int code;
// constructors
public AleaException() {
}
public AleaException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public AleaException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
public AleaException(String detailMessage, Throwable throwable, int code) {
super(detailMessage, throwable);
this.code = code;
}
// getters and setters
....
}
- line 3: [AleaException] extends the [RuntimeException] class. It is therefore an unhandled exception (no requirement to handle it with a try/catch);
- line 6: an error code is added to the [RuntimeException] class;
1.16.1.5. The web service / JSON
![]() |
![]() |
The web service / JSON is implemented by Spring MVC. Spring MVC implements the MVC (Model–View–Controller) architectural pattern as follows:
![]() |
The processing of a client request proceeds as follows:
- request - the requested URLs are in the form http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... The [Dispatcher Servlet] is the Spring class that handles incoming URLs. It "routes" the URL to the action that should handle it. These actions are methods of specific classes called [Controllers]. The C in MVC here refers to the chain [Dispatcher Servlet, Controller, Action]. If no action has been configured to handle the incoming URL, the [Dispatcher Servlet] will respond that the requested URL was not found (404 NOT FOUND error);
- processing
- the selected action can use the parameters that the [Dispatcher Servlet] has passed to it. These can come from several sources:
- the path [/param1/param2/...] of the URL,
- the URL parameters [p1=v1&p2=v2],
- from parameters posted by the browser with its request;
- when processing the user's request, the action may need the [business] layer [2b]. Once the client's request has been processed, it may trigger various responses. A classic example is:
- an error page if the request could not be processed correctly
- a confirmation page otherwise
- the action instructs a specific view to be displayed [3]. This view will display data known as the view model. This is the M in MVC. The action will create this M model [2c] and instruct a V view to be displayed [3];
- response - the selected view V uses the model M constructed by the action to initialize the dynamic parts of the HTML response it must send to the client, then sends this response.
For a web service / JSON, the previous architecture is slightly modified:
![]() |
- in [4a], the model, which is a Java class, is converted into a JSON string by a JSON library;
- in [4b], this JSON string is sent to the browser;
An example of serializing a Java object into a JSON string and deserializing a JSON string into a Java object is presented in the appendices in Section 6.14.
Let’s return to the [web] layer of our application:
![]() |
In our application, there is only one controller:
![]() |
The web/JSON service will send its clients a response of type [Response] as follows:
package exemples.android.server.web;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// 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
...
}
- line 13: the [T body] field is the response expected by the client. We decided to use a generic response of type T here, rather than the Integer type of the expected random number. We want to be able to reuse this class in other situations. While processing the client’s request, the server may encounter a problem, which is then summarized in the other two fields;
- line 8: a status code (0 if no error);
- line 9: if status != 0, a list of error messages—usually those from the exception stack if an exception occurred—null if there are no errors;
The controller [WebController] is as follows:
package exemples.android.server.web;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.server.metier.AleaException;
import examples.android.server.business.IMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.List;
@Controller
public class WebController {
// business layer
@Autowired
private IBusinessModel businessModel;
// JSON mapper
@Autowired
private ObjectMapper mapper;
// random numbers
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRandom(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
// the response
Response<Integer> response = new Response<>();
// we use the business layer
try {
response.setBody(business.getRandom(a, b));
response.setStatus(0);
} catch (AleaException e) {
response.setStatus(e.getCode());
response.setMessages(getMessagesFromException(e));
}
// return the response
return mapper.writeValueAsString(response);
}
private List<String> getMessagesFromException(Throwable e) {
// list of messages
List<String> messages = new ArrayList<String>();
// iterate through the exception stack
Throwable th = e;
while (th != null) {
messages.add(e.getMessage());
th = th.getCause();
}
// return the result
return messages;
}
}
- line 17: the [@Controller] annotation indicates that the class is an MVC controller whose methods handle requests for certain URLs in the web application;
- lines 21–22: the [@Autowired] annotation instructs Spring to inject a component of type [IMetier] into the field. This will be the previous [Metier] class. Because we added the [@Service] annotation to it, it is treated as a Spring component;
- lines 24–25: we do the same with a JSON mapper that we will define later. Our web service will send its response as a JSON string. This mapper will handle the serialization of the response into JSON;
- line 30: the method that generates the random number. Its name doesn’t matter. When it runs, its parameters have been initialized by Spring MVC. We’ll see how. Furthermore, if it runs, it’s because the web server received an HTTP GET request for the URL in line 28;
- line 28: the [@RequestMapping] annotation defines certain properties of the annotated method:
- [value]: the URL accepted by the method;
- [method]: the HTTP method accepted by the method. There are mainly two: GET and POST. The [POST] method is used when the client wants to attach a document to its HTTP request;
- [produces]: sets one of the headers of the HTTP response that will be sent to the client. Here, among the HTTP headers sent with the response to the client, there will be one that informs the client that the response is being sent in the form of a JSON string. This header is not mandatory. It is provided to the client for informational purposes if the client expects responses that may take various forms;
- [consumes]: not present here. It specifies the HTTP headers that must accompany the client’s HTTP request for it to be accepted;
- line 29: the [@ResponseBody] annotation indicates that the result produced by the method must be sent to the client. Without this annotation, the method’s response is treated as a key used to select the HTML page to send to the client. In a web service / JSON, there are no HTML pages;
- line 28: the processed URL is of the form /{a}/{b}, where {x} represents a variable. The variables {a} and {b} are assigned to the method’s parameters on line 30. This is done via the @PathVariable("x") annotation. Note that {a} and {b} are components of a URL and are therefore of type String. The conversion from String to the parameter type may fail. Spring MVC then throws an exception. To summarize: if I request the URL /100/200 in a browser, the getAlea method on line 30 will execute with the integer parameters a=100, b=200;
- line 36: the [business] layer is asked for a random number in the range [a,b]. Recall that the [business].getAlea method can throw an exception;
- line 37: no error;
- line 39: error code;
- line 40: the list of response messages is that of the exception stack (lines 46–57). Here, we know that the stack contains only one exception, but we wanted to demonstrate a more generic method;
- line 43: the response of type [Response<Integer>] is returned as a JSON string;
1.16.1.6. Spring Project Configuration
![]() |
There are various ways to configure Spring:
- using XML files;
- with Java code;
- using a combination of both;
We choose to configure our web application using Java code. The following [Config] class handles this configuration:
package exemples.android.server.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@ComponentScan(basePackages = { "examples.android.server.business", "examples.android.server.web" })
@EnableWebMvc
public class Config {
// web configuration ------------------------------------
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// JSON mapper
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- Line 12: We tell Spring in which packages it will find the two components it needs to manage:
- the [Metier] component annotated with [@Service] in the [exemples.android.server.metier] package;
- the [WebController] component annotated with [@Controller] in the [examples.android.server.web] package;
- line 13: the [@EnableWebMvc] annotation allows Spring Boot to automatically handle a number of standard configurations for a Spring MVC application. This significantly reduces the developer’s workload;
- lines 16, 22, 27, and 33: the [@Bean] annotation also defines Spring components (beans) in the same way as the two annotations encountered (@Service, @Controller). Here, the [@Bean] annotation annotates a method rather than a class, and the result of the method is the Spring component. In the absence of a naming attribute within the [@Bean] annotation, the Spring component created takes the name of the annotated method;
- lines 16–20: define the [dispatcherServlet] bean. This is a predefined name in Spring MVC that defines the front controller of the MVC application, an object through which all client requests pass and which dispatches them (hence its name) to the various [@Controller]s in the Spring MVC application;
- line 18: the [dispatcherServlet] bean is an instance of the [DispatcherServlet] class provided by Spring MVC;
- lines 22–25: the [servletRegistrationBean] bean is used to define which URLs are accepted by the application. On line 24, all URLs are accepted;
- lines 27–30: the [embeddedServletContainerFactory] bean is used to define the embedded server in the project dependencies that will host the web application. Line 29 specifies that this is a Tomcat server and that it will run on port 8080. By default, the binaries for this web server are provided by the [org.springframework.boot:spring-boot-starter-web] dependency in the Gradle file;
1.16.1.7. Running the web service / JSON
![]() |
The project runs from the following [Boot] executable class:
package exemples.android.server.boot;
import exemples.android.server.config.Config;
import org.springframework.boot.SpringApplication;
public class Boot {
public static void main(String[] args) {
// Run the application
SpringApplication.run(Config.class, args);
}
}
- The [Boot] class is an executable class (lines 7–10);
- line 9: the static method [SpringApplication.run] is a method of [Spring Boot] (line 4) that will launch the application. Its first parameter is the Java class that configures the project. Here, it is the [Config] class we just described. The second parameter is the array of arguments passed to the [main] method (line 7);
The web application can be launched in various ways, including the following:
![]() |
A number of logs then appear in the console:
- lines 12-14: the Tomcat embedded server is launched;
- lines 15-19: the Spring MVC servlet [DispatcherServlet] is loaded and configured;
- line 20: the web server URL [/{a}/{b}] is detected;
Now, let’s open a browser and test the web service’s JSON URL:
![]() |
![]() |
![]() |
![]() |
Each time, we get the JSON representation of an object of type [Response<Integer>].
Instead of using a standard browser, let’s now use the [ Advanced Rest Client] extension for the Chrome browser (see appendices, section 6.13):

- in [1], the requested URL;
- in [2], using a GET request;
- in [3], the request is sent;

- in [4], the HTTP headers of the server’s response. Note that this indicates the sent document is a JSON string;
- in [5], the received JSON string;
1.16.1.8. Generating the project’s executable JAR
In section 1.16.1.2, we showed how to configure the Gradle file to generate an executable for the application with all its dependencies. Adapted to the current application, this configuration becomes the following:
// create a binary with all its dependencies
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'examples.android.server.boot.Boot'
}
baseName = project.name + '-all'
from { configurations.compile.collect { if it.isDirectory() then it else zipTree(it) } }
with jar
}
To generate this executable, follow these steps [1-5]:
![]() | ![]() |
To run it, stop the web service if it is running [1], then run the archive [2-4]:
![]() | ![]() |
Open a browser and request the URL [localhost:8080/100/200]. You should get the same results as before.
1.16.1.9. Log Management
When you run the executable archive, you’ll notice that the logs are different from when you run the project from the IDE. You’ll see logs in [DEBUG] mode:
...
09:32:03.741 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletConfigInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletContextInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemProperties]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemEnvironment]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source. Returning [null]
June 7, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
June 7, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]
June 7, 2016 9:32:03 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFO: Using a shared selector for servlet write/read
09:32:03.810 [main] INFO org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:32:03.813 [main] INFO exemples.android.server.boot.Boot - Boot started in 1.984 seconds (JVM running for 2.206)
You can manage the log level by adding a [logback.xml] file to the project's [resources] folder:
![]() |
This file could have the following content:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- log level control -->
<root level="info"> <!-- info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
The log level is controlled on line 12. If we now rebuild the executable archive and run it, we only get [info] level logs:
...
09:36:52.433 [main] INFO o.h.validator.internal.util.Version - HV000001: Hibernate Validator 5.2.4.Final
09:36:52.762 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Tue Jun 07 09:36:51 CEST 2016]; root of context hierarchy
09:36:52.811 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/{a}/{b}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String exemples.android.server.web.WebController.getAlea(int,int) throws com.fasterxml.jackson.core.JsonProcessingException
June 7, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
June 7, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]
June 7, 2016 9:36:52 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFO: Using a shared selector for servlet write/read
09:36:52.923 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:36:52.926 [main] INFO exemples.android.server.boot.Boot - Boot started in 1.865 seconds (JVM running for 2.203)
1.16.2. The Android client for the web server / JSON
The Android client will have the following architecture:
![]() |
The client will have two components:
- a [Presentation] layer (view+activity) similar to the one we studied in Example [Example-14];
- the [DAO] layer that interacts with the [web / JSON] service we studied previously.
1.16.2.1. Creating the project
We duplicate the previous project [Example-14] in [Example-15] by following the procedure in section 1.4. We obtain the following result:
![]() | ![]() |
Next, the reader is invited to create the following project.
1.16.2.2. Gradle configuration
![]() |
The [build.gradle] file is as follows:
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 {
applicationId "examples.android"
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 fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
We will only comment on what has not already been covered:
- lines 46–47: insertion of an AA plugin. The [rest-spring-api] plugin allows client/server communication to be delegated to the AA library;
- line 50: the [spring-android-rest-template] library is the library used by AA to handle client/server communication. Version [2.0.0.M3] is a so-called 'milestone' version that is not found in the usual Maven repositories. Therefore, we must specify, in lines 56-59, the repository to use (line 58) to find the library;
- Line 51: a JSON library;
- lines 33–39: without this property, errors occur when generating the project’s APK binary;
1.16.2.3. The Android application manifest
![]() |
The [AndroidManifest.xml] file needs to be updated. By default, Internet access is disabled. It must be enabled using a special directive:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="examples.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 5: Internet access is allowed;
1.16.2.4. The [DAO] layer
![]() |
![]() |
1.16.2.4.1. The [IDao] interface of the [DAO] layer
The interface of the [DAO] layer will be as follows:
package exemples.android.dao;
public interface IDao {
// random number
int getRandom(int a, int b);
// Web service URL
void setWebServiceJsonUrl(String url);
// maximum wait time (ms) for the server response
void setTimeout(int timeout);
// Client wait time in milliseconds before sending a request
void setDelay(int delay);
}
- line 6: the web service / JSON method to obtain a random number in the range [a,b] from this web service;
- line 9: the URL of the web service / JSON for generating random numbers;
- line 12: we set a maximum timeout to wait for the server's response;
- line 15: we want to set a timeout before executing the request to the server, to give the user time to cancel their request;
1.16.2.4.2. The [WebClient] interface
![]() |
The [WebClient] interface handles communication with the web service. Its code is as follows:
package exemples.android.dao;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// 1 random number in the range [a,b]
@Get("/{a}/{b}")
Response<Integer> getRandom(@Path("a") int a, @Path("b") int b);
}
- Line 12: [WebClient] is an interface that the AA library will implement itself using the annotations we will add to it. This interface must implement calls to the URLs exposed by the web service / JSON:
// random number
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRandom(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
- Line 11: The [@Rest] annotation is an AA annotation. The value of the [converters] attribute is an array of converters. Here, the [MappingJackson2HttpMessageConverter.class] converter ensures that when the server sends a JSON string, it is automatically deserialized. Thus, we see in line (d) that the URL [/{a}/{b}] returns a String type, which is actually a JSON string (line b). With this information and that of the expected type on line 16, the client’s [WebClient] instance will deserialize the string it receives into a [Response<Integer>] type;
- line 15: an @Get annotation indicating that the URL must be called using an HTTP GET method. The parameter of the @Get annotation is the URL format expected by the web service. Simply use the [value] parameter from the @RequestMapping annotation (line b) of the method called in the server’s [WebController]. The curly braces {} enclose the URL parameters that must be passed to the method’s parameters on line 16. The syntax [@Path("a") int a] causes the method’s [a] parameter to be assigned the value {a} from the URL. When the URL parameter and the method parameter have the same name, as here, we can write more simply [@Path int a];
In the case of an HTTP POST request, the call method would have the following signature:
@Post("/{a}/{b}")
Response<Integer> getAlea(@Body T body, @Path("a") int a, @Path("b") int b);
The [@Body] annotation designates the posted value. This will be automatically serialized to JSON. On the server side, we will have the following signature:
// random numbers
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b, @RequestBody T body) {
- line 2: specifies that an HTTP POST request is expected and that the request body (posted object) must be transmitted as a JSON string (consumes attribute);
- line 4: the posted value will be retrieved in the method’s [@RequestBody T body] parameter;
Let’s return to the code for the [WebClient] class:
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
- We need to be able to specify the URL of the web service to contact. This is achieved by extending the [RestClientRootUrl] interface provided by AA. This interface exposes a [setRootUrl(urlServiceWeb)] method that allows us to set the URL of the web service to contact;
- Furthermore, we want to control the call to the web service because we want to limit the response wait time. To do this, we extend the [RestClientSupport] interface, which exposes the [setRestTemplate] method that will allow us to:
- create the [RestTemplate] object ourselves, which is used to manage client/server exchanges;
- configure this object to set the maximum response timeout;
1.16.2.4.3. The [Response] class
The [getAlea] method of the [IDao] interface returns a response of type [Response] as follows:
package exemples.android.dao;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// 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
...
}
This is the [Response] class already used on the server side (section 1.16.1.5). In fact, from a programming perspective, it is as if the client’s [DAO] layer were communicating directly with the web service’s [WebController]:
![]() |
Network communication between client and server, as well as the serialization/deserialization of Java objects on the client side, are transparent to the programmer.
1.16.2.4.4. Implementation of the [DAO] layer
![]() |
The [IDao] interface is implemented with the following [Dao] class:
package exemples.android.dao;
import com.fasterxml.jackson.databind.ObjectMapper;
import examples.android.architecture.Utils;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
@EBean
public class Dao implements IDao {
// REST service client
@RestService
protected WebClient webClient;
// JSON mapper
private ObjectMapper mapper = new ObjectMapper();
// delay before executing the request
private int delay;
// IDao interface -------------------------------------------------------------------
@Override
public int getRandom(int a, int b) {
...
}
@Override
public void setWebServiceJsonUrl(String webServiceJsonUrl) {
...
}
@Override
public void setTimeout(int timeout) {
...
}
@Override
public void setDelay(int delay) {
this.delay = delay;
}
}
- line 15: we annotate the [Dao] class with the [@EBean] annotation to turn it into an AA bean that we can inject elsewhere;
- lines 19–20: we inject the implementation of the [WebClient] interface that we have described. The [@RestService] annotation handles this injection;
- the other methods implement the [IDao] interface (lines 27–46);
[setTimeout] method
The [setTimeout] method is as follows:
@Override
public void setTimeout(int timeout) {
// Set the timeout for REST client requests
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
// Create the RestTemplate
RestTemplate restTemplate = new RestTemplate(factory);
// Set the JSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// Set the RestTemplate for the web client
webClient.setRestTemplate(restTemplate);
}
- The [WebClient] interface will be implemented by a class AA using the Gradle dependency [org.springframework.android:spring-android-rest-template]. [spring-android-rest-template] implements the client's communication with the web/JSON server using a [RestTemplate] class;
- line 4: the [SimpleClientHttpRequestFactory] class is provided by the [spring-android-rest-template] dependency. It will allow us to set the maximum timeout for the server response (lines 5-6);
- line 8: we construct the [RestTemplate] object, which will serve as the communication channel with the web service. We pass the [factory] object that was just constructed as a parameter to it;
- line 10: the client/server dialogue can take various forms. Exchanges occur via text lines, and we must tell the [RestTemplate] object what to do with each text line. To do this, we provide it with converters—classes capable of processing text lines. The choice of converter is generally made via the HTTP headers accompanying the text line. Here, we know that we are receiving only text lines in JSON format. Furthermore, as we saw in section 1.16.1.7, the server sent the HTTP header:
Content-Type: application/json;charset=UTF-8
Line 10: the only converter for the [RestTemplate] will be a JSON converter implemented using the [Jackson] library. There is a quirk regarding these converters: AA requires us to include it in the [WebClient] annotation as well:
@Rest(converters = {MappingJacksonHttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
Line 1: We are required to specify a converter even though we are already specifying it programmatically.
- Line 12: The [RestTemplate] object constructed in this way is injected into the implementation of the [WebClient] interface, and it is this object that will handle the client/server communication;
Method [getAlea]
The [getAlea] method is as follows:
@Override
public int getAlea(int a, int b) {
// service execution
Response<Integer> info;
DaoException ex;
try {
// wait
waitSomeTime(delay);
// execute service
info = webClient.getAlea(a, b);
int status = info.getStatus();
if (status == 0) {
// return the result
return info.getBody();
} else {
// log the exception
ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
}
} catch (JsonProcessingException | RuntimeException e) {
// log the exception
ex = new DaoException(e, 100);
}
// throw the exception
throw ex;
}
...
// private methods -------------------
private void waitSomeTime(int delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- line 8: wait [delay] milliseconds;
- line 10: we simply call the method with the same signature in the class implementing the [WebClient] interface;
- line 11: we analyze the response received from the server by checking its [status];
- lines 12–14: if there was no server-side error (status = 0), then return the method’s result;
- line 17: if there was a server-side error (status!=0), then we prepare an exception without throwing it. The server has sent a list of error messages. We create an exception with, as its sole message, the JSON string of the server’s message list;
- lines 19–22: other exception cases;
- line 24: when we reach this point, an exception has necessarily occurred. So we throw it;
The [DaoException] used by this code is as follows:
package exemples.android.dao;
import java.util.ArrayList;
import java.util.List;
public class DaoException extends RuntimeException {
// error code
private int code;
// constructors
public DaoException() {
}
public DaoException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public DaoException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
// getters and setters
...
}
- line 6: the [DaoException] is an unhandled exception;
Method [setUrlServiceWebJson]
The [setUrlServiceWebJson] method is as follows:
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// Set the REST service URL
webClient.setRootUrl(urlServiceWebJson);
}
- Line 4: We set the web service URL using the [setRootUrl] method of the [WebClient] interface. This method exists because this interface extends the [RestClientRootUrl] interface;
1.16.2.5. The [architecture] package
The [architecture] package contains the elements that structure the application:
![]() |
![]() |
1.16.2.5.1. The [IMainActivity] interface
The [IMainActivity] interface lists the methods that the application's activity must implement:
package exemples.android.architecture;
import exemples.android.dao.IDao;
public interface IMainActivity extends IDao {
// access to the session
Session getSession();
// change view
void navigateToView(int position);
// wait
void beginWaiting();
void cancelWaiting();
// debug mode
boolean IS_DEBUG_ENABLED = true;
// response timeout
int TIMEOUT = 1000;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
}
- line 5: the [IMainActivity] interface extends the [IDao] interface;
- lines 13–16: to the methods already present in the previous examples (lines 7–11), we have added two methods to manage the application’s loading screen (lines 14, 16);
- line 21: we set a maximum timeout for the server response to 1 second;
1.16.2.5.2. The [Utils] class
We have grouped static utility methods in the [Utils] class that can be called from various parts of the application architecture:
package exemples.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 with 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 getMessagesForAlert(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();
}
}
- lines 9–18: creates a list of error messages contained in a Throwable;
- lines 21-32: uses the previous method to construct, from the list of messages obtained, the text to be displayed in an Android alert message;
- lines 27-28: the messages are numbered. The smallest number (1) corresponds to the initial exception, and the highest number to the most recent exception in the exception stack;
1.16.2.5.3. The abstract class [AbstractFragment]
The [AbstractFragment] class has two purposes:
- to ensure that the [updateFragments] method of the child classes is always called when the fragment is displayed, and only once;
- to factor out the state and methods of the child classes that can be factored out;
It is purpose 2 that leads us to include wait image management operations in this class: all components of an asynchronous Android application must handle this type of issue:
// wait management
protected void beginWaiting() {
// display the hourglass
mainActivity.beginWaiting();
}
protected void cancelWaiting() {
// remove the hourglass
mainActivity.cancelWaiting();
}
1.16.2.6. The view
![]() |
1.16.2.6.1. The view [view1.xml]
![]() |
Compared to the previous example, the view [view1.xml] has changed as follows:
![]() |
![]() |
- in [1], the user must specify the web service URL and the timeout [2] before each call to the web service;
- in [3], responses are counted;
- in [4], the user can cancel their request;
- in [5], a loading indicator appears when the numbers are requested. It disappears once all numbers have been received or the operation has been canceled;

- In [6], the validity of the entries is checked;
The reader is invited to load the file [vue1.xml] from the examples. For the rest of this section, we provide the IDs of the new components:

Buttons [10-11] are physically on top of each other. At any given time, only one of the two will be visible.
1.16.2.6.2. The [Vue1Fragment] fragment
![]() |
The skeleton of the [Vue1Fragment] fragment is as follows:
package exemples.android.fragments;
import android.app.AlertDialog;
import android.support.annotation.*;
import android.support.v4.app.Fragment;
import android.view.View;
import android.widget.*;
import examples.android.R;
import examples.android.architecture.AbstractFragment;
import examples.android.architecture.Utils;
import org.androidannotations.annotations.*;
import org.androidannotations.annotations.UiThread;
import org.androidannotations.api.BackgroundExecutor;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// Visual interface elements
@ViewById(R.id.editTextUrlServiceWeb)
EditText edtUrlServiceRest;
@ViewById(R.id.errorUrlTextView)
TextView txtWebServiceUrlErrorMessage;
@ViewById(R.id.editTextDelay)
EditText edtDelay;
@ViewById(R.id.textViewErrorDelay)
TextView errorDelayTextView;
@ViewById(R.id.lst_reponses)
ListView listAnswers;
@ViewById(R.id.txt_Answers)
TextView infoAnswers;
@ViewById(R.id.edt_nbaleas)
EditText edtNumberOfShots;
@ViewById(R.id.edt_a)
EditText edtA;
@ViewById(R.id.edt_b)
EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
TextView txtErrorRandom;
@ViewById(R.id.txt_errorInterval)
TextView txtErrorInterval;
@ViewById(R.id.btn_Execute)
Button btnExecute;
@ViewById(R.id.btn_Cancel)
Button btnCancel;
...
// local data
private List<String> answers;
private ArrayAdapter<String> adapterAnswers;
@AfterViews
void afterViews() {
// memory
afterViewsDone = true;
// initially no error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorInterval.setVisibility(View.INVISIBLE);
txtWebServiceErrorUrl.setVisibility(View.INVISIBLE);
textViewErrorDelay.setVisibility(View.INVISIBLE);
// [Cancel] button hidden
btnCancel.setVisibility(View.INVISIBLE);
btnExecute.setVisibility(View.VISIBLE);
// list of responses
answers = new ArrayList<>();
}
...
- lines 24–49: references to the view components [view1.xml] (line 20);
- lines 55-69: the [@AfterViews] method executed when the references in lines 24-49 have been initialized;
- line 58: don’t forget this—necessary for the fragment’s lifecycle;
- lines 60–63: error messages are hidden;
- lines 65–66: the [Cancel] button is hidden (line 65) and the [Execute] button is displayed (line 66). Note that they are physically on top of each other;
- Line 68: The field on line 52 will contain the list of strings to be displayed by the ListView of responses;
Immediately after the [@AfterViews] method, the following [updateFragment] method will be executed:
@Override
protected void updateFragment() {
// create the adapter for the list of responses
adapterAnswers = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, answers);
listAnswers.setAdapter(answerAdapter);
}
- Lines 4-5: Create the ListView adapter for the answers. It is stored in an instance variable so it is available to other methods in the class;
Clicking the [Execute] button triggers the execution of the following method:
// the inputs
private int nbRandom;
private int a;
private int b;
private String webServiceJsonUrl;
private int delay;
// local data
private int countOfInfo;
private List<String> responses;
private ArrayAdapter<String> responseAdapter;
private boolean hasBeenCanceled;
@Click(R.id.btn_Execute)
protected void doExecute() {
// clear previous responses
answers.clear();
adapterAnswers.notifyDataSetChanged();
hasBeenCanceled = false;
// reset the response counter to 0
nbInfos = 0;
infoAnswers.setText(String.format("List of answers (%s)", nbAnswers));
// Check the validity of the entries
if (!isPageValid()) {
return;
}
// initialize activity
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// request random numbers
for (int i = 0; i < nbAleas; i++) {
getRandom(a, b);
}
// start waiting
beginWaiting();
}
@Background(id = "alea")
void getRandom(int a, int b) {
// We should do as little as possible here
// In any case, no display—these should be handled in the UiThread
try {
// display the result in the UiThread
showInfo(mainActivity.getAlea(a, b));
} catch (RuntimeException e) {
// display the exception in the UiThread
showAlert(e);
}
}
- lines 17–18: we clear the previous list of responses from the server. To do this, on line 17, we clear the data source [reponses] associated with the ListView adapter;
- line 19: a boolean that will tell us whether or not the user canceled their request;
- lines 21-22: we display a counter set to zero for the number of responses;
- lines 24-26: We retrieve the entries from lines [2-6] and verify their validity. If any of them is invalid, the method is aborted (line 25) and the user is returned to the visual interface;
- lines 28-29: if all entered data is valid, then the web service URL (line 28) and the wait time before each service call (line 29) are passed to the activity. This information is required by the [DAO] layer, and note that it is the activity that communicates with it;
- lines 31–33: random numbers are requested one by one from the [getAlea] method on line 39;
- line 38: the [getAlea] method is annotated with the AA [@Background] annotation, which means it will be executed in a different thread (execution flow, process) than the one in which the visual interface runs. It is indeed mandatory to execute any internet call in a thread different from that of the visual interface. Thus, at any given time, there may be several threads:
- the one that displays the UI (User Interface) and manages its events,
- the [nbAleas] threads, each of which requests a random number from the web service. These threads are launched asynchronously: the UI thread launches a [getAlea] thread (line 32) that requests a random number from the web service and does not wait for it to finish. It will be notified of completion via an event. Thus, the [nbAleas] threads will be launched in parallel. It is possible to configure the application so that it launches only one thread at a time. In that case, there is a queue of threads to be executed;
Line 38: the [id] parameter assigns a name to the generated thread. Here, the [nbAleas] threads all have the same name [alea]. This will allow us to cancel them all at the same time. This parameter is optional if thread cancellation is not handled;
- Line 44: The activity’s [getAlea] method is called. It will therefore be executed in a separate thread from the UI. This thread will make the call to the web service and will not wait for the response. It will be notified later via an event that the response is available. At this point, on line 44, the [showInfo] method will be called with the received response as a parameter;
- Lines 45–47: Executing the web request may throw an exception. We then request that the exception’s error messages be displayed in an alert message;
- Line 35: We wait for the results:
- a loading indicator will be displayed;
- the [Cancel] button will replace the [Execute] button. Because the launched threads are asynchronous, the UI thread does not wait for them, and line 35 is executed before they finish. Once the [beginWaiting] method has finished, the UI can once again respond to user in , such as a click on the [Cancel] button. If the launched threads had been synchronous, line 35 would only be reached once all threads had finished. Canceling them would then no longer make sense;
The [showInfo] method is as follows:
@UiThread
protected void showInfo(int alea) {
if (!hasBeenCanceled) {
// one more piece of info
nbInfos++;
infoReponses.setText(String.format("List of responses (%s)", nbInfos));
// Are we done?
if (nbInfos == nbAleas) {
// end the wait
cancelWaiting();
}
// add the information to the list of responses
answers.add(0, String.valueOf(alea));
// display the responses
adapterAnswers.notifyDataSetChanged();
}
}
- The [showInfo] method is called within the [getAlea] thread annotated with [@Background]. This method will update the UI. It can only do so by running within the UI thread. This is the meaning of the [@UiThread] annotation on line 1;
- line 2: the method receives a random number;
- line 3: the body of the method is executed only if the user has not canceled their request;
- lines 5–6: the response counter is incremented and displayed;
- lines 8–11: if all expected responses have been received, the wait is terminated (end of the wait signal; the [Execute] button replaces the [Cancel] button);
- lines 12–15: the received random number is added to the list of responses displayed by the [ListView listReponses] component, and the list is refreshed;
The [showAlert] method is as follows:
@UiThread
protected void showAlert(Throwable th) {
if (!hasBeenCanceled) {
// cancel everything
cancelAll();
// display it
new AlertDialog.Builder(activity).setTitle("Errors have occurred").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Close", null).show();
}
}
The logic here is similar to that of the [showInfo] method:
- line 1: the [@UiThread] annotation is required;
- line 2: the method receives the exception that occurred;
- line 3: the method is executed only if the user has not canceled their request;
- line 5: the user’s request is canceled as if they had clicked the [Cancel] button themselves;
- line 7: the alert is displayed using the Android [AlertDialog] class:
- [activity]: is the [Activity] type activity stored in the parent class [AbstractFragment];
- [setTitle]: sets the title of the alert window [1];
- [setMessage]: sets the message displayed by the alert window [2];
- [setNeutral]: sets the button that will close the alert window [3];
- [show]: requests that the alert window be displayed;
![]() |
Clicking the [Cancel] button is handled with the following method:
@Click(R.id.btn_Cancel)
protected void doCancel() {
// memory
hasBeenCanceled=true;
// cancel the asynchronous task
BackgroundExecutor.cancelAll("alea", true);
// end of wait
cancelWaiting();
}
- line 4: note that the user has canceled their request;
- line 6: cancels all tasks identified by the string [alea]. The second parameter [true] means that they must be canceled even if they have already been launched. The identifier [alea] is the one used to qualify the [getAlea] method of the fragment (line 1 below):
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
Note: It turned out that line 6 of the [doAnnuler] method’s code was behaving incorrectly. This is why we added the boolean [hasBeenCanceled]. Indeed, in the event of an exception (server down), the alert window would appear n times if we had requested n random numbers.
1.16.2.7. The [MainActivity] activity
![]() |
1.16.2.7.1. The [activity-main.xml] view
![]() |
Compared to the previous example, we added a loading image to the view associated with the [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">
<!-- loading image -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- loading image -->
</android.support.design.widget.AppBarLayout>
...
- lines 17-21: the placeholder image;
1.16.2.7.2. The [MainActivity] activity
The [MainActivity] has changed little from what it was in [Example-14]. First, we inject the [DAO] layer into it:
// DAO injection
@Bean(Dao.class)
protected IDao dao;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// configure the [DAO] layer
setTimeout(TIMEOUT);
}
- lines 2-3: injection of the [DAO] layer via an AA annotation;
- lines 5-13: code executed after this injection;
- line 12: set the timeout for the [DAO] layer
Additionally, the [MainActivity] activity must implement the [IMainActivity] interface, which itself extends the [IDao] interface:
// IMainActivity implementation --------------------------------------------------------------------
@Override
public void navigateToView(int position) {
// display the view at position
if (mViewPager.getCurrentItem() != position) {
// display fragment
mViewPager.setCurrentItem(position);
}
}
// Handling the loading image
public void cancelWaiting() {
loadingPanel.setVisibility(View.INVISIBLE);
}
public void beginWaiting() {
loadingPanel.setVisibility(View.VISIBLE);
}
// IDao implementation --------------------------------------------------------------------
@Override
public int getRandom(int a, int b) {
// execution
return dao.getRandom(a, b);
}
@Override
public void setDelay(int delay) {
dao.setDelay(delay);
}
@Override
public void setWebServiceJsonUrl(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
1.16.2.8. Running the project
Start the web service (section 1.16.1.7) and then launch the Android client:

To find out what to enter in [1], follow these steps. Open a command prompt and type the following command:
C:\Program Files\Console2>ipconfig
Windows IP Configuration
Wireless Network Adapter Local Area Connection* 3:
Media status. . . . . . . . . . . . : Media disconnected
Connection-specific DNS suffix. . . :
VirtualBox Host-Only Network Ethernet adapter:
Connection-specific DNS suffix. . . :
Local loopback IPv6 address. . . . .: fe80::e481:1583:cd2a:c47%27
IPv4 address. . . . . . . . . . . . . .: 192.168.82.2
Subnet mask. . . . . . . . . . . . . .: 255.255.255.0
Default gateway. . . . . . . . . :
VirtualBox Host-Only Network #2 Ethernet Card:
Connection-specific DNS suffix. . . :
Local link IPv6 address . . . . .: fe80::8191:14ad:407d:b840%54
IPv4 address . . . . . . . . . . . . . .: 192.168.64.2
Subnet mask. . . . . . . . . . . . . .: 255.255.255.0
Default gateway. . . . . . . . . :
Ethernet card:
Connection-specific DNS suffix: ad.univ-angers.fr
Local link IPv6 address . . . . .: fe80::d972:ad53:3b8a:263f%28
IPv4 address . . . . . . . . . . . . . .: 172.19.81.34
Subnet mask. . . . . . . . . . . . . .: 255.255.0.0
Default gateway. . . . . . . . . : 172.19.0.254
Wi-Fi wireless network adapter:
Media status. . . . . . . . . . . . : Media disconnected
Connection-specific DNS suffix. . . : uang ad.univ-angers.fr univ-angers.fr
If you have installed [GenyMotion], the VirtualBox virtual machine has added IP addresses to your computer (lines 10 and 18). These addresses are particularly convenient because they are not blocked by the Windows firewall. Line 30 shows your computer’s IP address on a local network. To use this address, you generally need to disable the Windows firewall. If you are connected to a Wi-Fi network, use the Wi-Fi address and, in this case as well, disable the firewall if you have one.
Test the application in the following cases:
- 100 random numbers in the range [1000, 2000] without a timeout;
- 2000 random numbers in the range [10000, 20000] without a timeout, and cancel the wait before generation completes;
- 5 random numbers in the range [100, 200] with a wait time of 5000 ms, and cancel the wait before generation completes;
1.16.2.9. Cancelation Handling
To track what happens when the user requests cancellation or when cancellation is triggered by an exception, we add the following method to the [IDao] interface (see section 1.16.2.4.1):
package exemples.android.dao;
public interface IDao {
...
// debug mode
void setDebugMode(boolean isDebugEnabled);
}
In the [Dao] class, we add the following code:
// debug mode
private boolean isDebugEnabled;
// class name
private String className;
..
// constructor
public Dao() {
// class name
className = getClass().getSimpleName();
}
...
// IDao interface -------------------------------------------------------------------
@Override
public int getRandom(int a, int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] in progress", a, b));
}
// service execution
Response<Integer> info;
...
@Override
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
- line 9: we note the class name;
- lines 16–18: We write a log every time the [getAlea] method is called;
Additionally, in the [Vue1Fragment] fragment, we add the following logs:
@UiThread
protected void showInfo(int alea) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
....
}
@UiThread
protected void showAlert(Throwable th) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception received");
}
...
}
}
@Click(R.id.btn_Cancel)
protected void doCancel() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Cancellation requested");
}
...
}
Every time the [Vue1Fragment] fragment receives information from the [DAO] layer, a log is issued. Additionally, when the [doAnnuler] method is called, the event is logged.
Test 1
We request 5 numbers even though the server has not been started. We get the following logs:
- Lines 1–5: The [getAlea] method of the [Dao] class is called five times. Note that these are asynchronous calls made by the [VueFragment] fragment, and that the fragment does not wait for the result of its call;
- line 7: the first HTTP request has been made, and the [VueFragment] fragment has received its first exception;
- line 8: it then requests the cancellation of all requests;
- lines 9–12: however, we see that it receives the following four exceptions. Therefore, the pending asynchronous requests were all executed;
Test 2
Now, let’s start the server and request 5 numbers with a 5-second delay, then click [Cancel] before the delay ends. The logs are as follows:
- lines 1-5: the [getAlea] method of the [Dao] class is called five times;
- line 7: the user requested that the requests be canceled;
- line 8: we see that [Vue1_Fragment] receives 5 values. Once again, the pending asynchronous requests have all been executed;
This is why we had to manage a boolean [hasBeenCanceled] to avoid displaying anything when a cancellation had been requested. In the cancellation code:
@Click(R.id.btn_Cancel)
protected void doCancel() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Cancellation requested");
}
// memory
hasBeenCanceled = true;
// cancel the asynchronous task
BackgroundExecutor.cancelAll("alea", true);
// end of wait
cancelWaiting();
}
The code on line 10 does not do what is expected. This may be because the asynchronous tasks share the same method annotated with [@Background]:
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
1.17. Example-16: Handling Asynchrony with RxAndroid
We will now manage the asynchrony required for Android applications using a library called RxJava [http://reactivex.io/] and its derivative version for the Android environment [RxAndroid]. To do this, we will use the course [Introduction to RxJava. Application to Swing and Android Environments].
1.17.1. Creating the project
We duplicate the [Example-1] project into [Example-16]:
![]() | ![]() |
1.17.2. Gradle Configuration
![]() |
In [build.gradle], we add the dependency on the [RxAndroid] library:
dependencies {
...
compile 'io.reactivex:rxandroid:1.2.0'
}
1.17.3. The [DAO] layer
![]() |
1.17.4. The [IDao] interface
The [IDao] interface becomes the following:
package examples.android.dao;
import rx.Observable;
public interface IDao {
// random number
Observable<Integer> getRandom(int a, int b);
// Web service URL
void setWebServiceJsonUrl(String url);
// maximum wait time (ms) for the server response
void setTimeout(int timeout);
// Client wait time in milliseconds before sending a request
void setDelay(int delay);
// debug mode
void setDebugMode(boolean isDebugEnabled);
}
- line 8: the [getAlea] method now returns an [Observable] type from the RxJava library (line 3). The principle is as follows:
A stream of elements of type Observable<T> is observed by one or more subscribers (observers, consumers) of type Subscriber<T>. The RxJava library allows the Observable<T> stream to run in thread T1 and its Subscriber<T> observer in thread T2 without the developer having to worry about managing the lifecycle of these threads and naturally difficult issues, such as data sharing between threads and thread synchronization to execute a global task. It therefore facilitates asynchronous programming.
1.17.5. The [AbstractDao] class
We will derive the [Dao] class from the following [AbstractDao] class:
package examples.android.dao;
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();
// protected methods ----------------------------------------------------------
// generic interface
protected interface IRequest<T> {
Response<T> getResponse();
}
// Generic request
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// service execution
return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// service execution
try {
// make the synchronous request and forward the response to the subscriber
Response<T> response = request.getResponse();
// error?
int status = response.getStatus();
if (status != 0) {
// Log the exception
ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
} else {
// send the response
subscriber.onNext(response.getBody());
// signal the end of the observable
subscriber.onCompleted();
}
} catch (JsonProcessingException | RuntimeException e) {
// log the exception
ex = new DaoException(e, 100);
}
// exception?
if (ex != null) {
// throw the exception
subscriber.onError(ex);
}
}
});
}
}
- The [AbstractDao] class has as its main element a generic method [getResponse] used to retrieve a [Response<T>] from the server, where T is the type of the result desired by the HTTP client (here, Integer);
- Line 20: The only parameter of the generic method [getResponse] is an instance of the generic interface [IRequest<T>] from lines 15–17. This interface has only one method [getResponse], and it is this method that returns the desired [Response<T>];
- Thanks to the two preceding points, the [AbstractDao] class can serve as a parent class for any client-side [Dao] layer of a server sending responses of type [Response<T>];
- line 20: the generic method [getResponse] returns a type [Observable<T>] that represents the result actually expected by the HTTP client (here, a type Observable<Integer>);
- lines 22–51: the static method [rx.Observable.create] creates an [Observable] type;
- line 22: the only parameter of this method is an instance of type [rx.Observable.OnSubscribe<T>], an interface that has the following methods:
- [onNext(T element)]: allows an element of type T to be emitted to an observer;
- [onError(Throwable th)]: allows an exception to be emitted to an observer;
- [onCompleted]: allows you to indicate to an observer that the emissions have ended;
A type [Observable<T>] obeys certain constraints:
- it emits its elements using the [onNext(T element)] method;
- the [onCompleted] method must be called exactly once as soon as there are no more elements to emit to the observer;
- the [onCompleted] method is not called if the [onError(Throwable th)] method has been called;
In our example:
- the observer will be the fragment [Vue1Fragment]. It is the observer that consumes the elements emitted by the [Observable<T>] (element or exception);
- the created [Observable<T>] type will emit only a single element (line 37);
- line 29: makes a synchronous HTTP request to the server and obtains the [Response<T>] type. This HTTP request is handled by the [IRequest] type passed as a parameter to the generic method [getResponse];
- line 31: retrieves the response status;
- lines 32–34: if this status indicates an error, an exception is prepared;
- lines 36–39: if the status is not an error, the response actually expected by the client is sent (line 37), and the observer is notified that there will be no further emissions (line 39);
- lines 41–44: if the HTTP request ends with an exception, log it;
- lines 46–49: if the exception [ex] is not null, then it is emitted to the observer. There is no need here to call the [onCompleted] method to indicate to the observer that no further elements will be emitted. This is implicit;
The key takeaway from these explanations is that:
- the generic method [<T> Observable<T> getResponse(final IRequest<T> request)] returns a type [Observable<T>] that emits either a single element of type T or an exception;
- this method accepts as its sole parameter a type [IRequest<T>] whose sole method [getResponse()] performs the HTTP request that returns the type [Response<T>];
1.17.6. The [Dao] class
The [Dao] class evolves as follows:
@EBean
public class Dao extends AbstractDao implements IDao {
// REST service client
@RestService
protected WebClient webClient;
// delay before executing the request
private int delay;
// debug mode
private boolean isDebugEnabled;
// class name
private String className;
// constructor
public Dao() {
// class name
className = getClass().getSimpleName();
}
// IDao interface -------------------------------------------------------------------
@Override
public Observable<Integer> getRandom(final int a, final int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] in progress", a, b));
}
// web client execution
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// wait
waitSomeTime(delay);
// synchronous HTTP call
return webClient.getAlea(a, b);
}
});
}
...
- line 2: the [Dao] class extends the [AbstractDao] class;
- line 24: the [getAlea] method now returns a type [Observable<Integer>];
- line 30: call to the generic method [getResponse] of the parent class. It is passed a parameter of type [IRequest<Integer>];
- lines 32–37: implementation of the [IRequest<Integer>] interface;
- line 36: the HTTP request is made via the AA [webClient] interface as was done previously. We know that we will retrieve a type [Response<Integer>], which is indeed the type that the method [IRequest<Integer>.getResponse()] must return;
- line 36: here we use a feature called a closure: the ability to encapsulate values external to an instance within it when it is created, in this case the values of [a, b] from line 24. This is what allows the method [IRequest<Integer>.getResponse()] to have no parameters. These values have been embedded within the body of the method. And where we would normally change the method’s parameters (a, b) -> (x, y), here we create a new instance of [IRequest<Integer>] encapsulating the values of x and y;
1.17.7. The [MainActivity] class
The [MainActivity] class, which implements the [IDao] interface, evolves as follows:
// IDao implementation --------------------------------------------------------------------
@Override
public Observable<Integer> getRandom(int a, int b) {
// execution
return dao.getAlea(a, b);
}
1.17.8. The [Vue1Fragment] class
The [Vue1Fragment] class evolves as follows:
@Click(R.id.btn_Executer)
protected void doExecute() {
// clear previous responses
answers.clear();
adapterAnswers.notifyDataSetChanged();
hasBeenCanceled = false;
// reset the response counter to 0
nbInfos = 0;
infoAnswers.setText(String.format("List of answers (%s)", nbAnswers));
// Check the validity of the entries
if (!isPageValid()) {
return;
}
// initialize activity
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// Generate random numbers
getRandomNumbersInBackground(a, b);
// start waiting
beginWaiting();
}
- line 18: request random numbers from the [getAleasInBackground] method, so named because the numbers will be requested in a thread different from the UI thread;
private int nbResponses = 0;
// subscriptions to observables
private List<Subscription> subscriptions;
// [Background] annotation is unnecessary
void getRandomNumbersInBackground(int a, int b) {
// Initially, no responses and no subscriptions
nbResponses = 0;
subscriptions.clear();
// prepare the observable
Observable<Integer> response = Observable.empty();
// merge the results of the various HTTP calls
// they are executed on an I/O thread
for (int i = 0; i < nbAleas; i++) {
response = response.mergeWith(mainActivity.getRandom(a, b).subscribeOn(Schedulers.io()));
}
// the merged observable will be observed on the UI thread
response = response.observeOn(AndroidSchedulers.mainThread());
try {
// execute the observable
subscriptions.add(response.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
// add the information to the list of responses
showInfo(alea);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
// error message
showAlert(th);
// end wait
doCancel();
}
}, new Action0() {
@Override
public void call() {
// end wait
cancelWaiting();
}
}));
} catch (RuntimeException e) {
// display the exception in the UI thread
showAlert(e);
}
}
- line 3: an observable has subscribers. The link between a subscriber and the process it observes is called a subscription. Here, we will have only one observed process and one subscriber. Therefore, we will have only one subscription. For the sake of the principle, we are treating it as if we could have multiple observed processes monitored by different observers, which would result in multiple subscriptions;
- lines 11–18: we configure the observed process (observable). It is important to understand that this is only configuration: the process is not executed;
- line 11: we start with an empty observable, an observable that emits nothing;
- lines 14–16: to this empty observable, we add [nbAleas] observables, which will be [nbAleas] HTTP requests that return [nbAleas] random numbers;
- Line 15: As before, the random number i is requested from the [MainActivity] class. It is important to understand that no HTTP request has been executed yet. The method [mainActivity.getRandom(a, b)] is executed and returns an [Observable<Integer>]. This is a process that will be observed once it is launched;
- line 15: the [subscribeOn(Schedulers.io())] method requests that the process be executed (when it is) on an I/O thread. The RxJava library offers different types of threads. The I/O thread is suitable for HTTP requests;
- line 15: observable #i is merged with the initial observable from line 11: from [nbAleas] Observables, each emitting one element, we create an observable that will emit [nbAleas] elements. This is the one that will be observed. This observable emits the [onCompleted] notification when all the observables that compose it have emitted their own [onCompleted] notifications. This will save us from having to count the responses, as we did in the previous version, to determine whether we have received all the expected numbers;
- line 18: by this point, we have configured an observable that is the composition of [nbAleas] observables, each running on an I/O thread;
- line 18: the [observeOn(AndroidSchedulers.mainThread())] method specifies on which thread the values emitted by the observable should be observed. Here, the thread [AndroidSchedulers.mainThread())] belongs to the RxAndroid library, not RxJava. It refers to the UI thread, also known as the event loop. This point is important: in an Android app, modifying a UI component can only be done on the UI thread; otherwise, an exception occurs;
- lines 19–45: now that the process to be observed has been configured, we execute it;
- line 21: the [Observable.subscribe] operation initiates the execution of the observed process. This operation will launch the [nbAleas] asynchronous processes configured earlier. The results of these processes will be automatically made available to the observer on the UI thread;
- Recall that the observable emits three types of events:
- [onNext]: when it emits an element;
- [onError]: when it encounters an exception;
- [onCompleted]: when it signals that it will no longer emit;
The [Observable.subscribe] method takes three objects as parameters: [Action1<Integer>, Action1<Throwable>, Action0], whose [call] methods are used to handle each of these three events;
- lines 21–27: the first parameter of type [Action1<Integer>] is used to handle the [onNext] event. Its [call] method receives the element emitted by the observable (line 23);
- line 25: we reuse the [showInfo] method from the previous example;
- lines 27–35: the second parameter of type [Action1<Throwable>] is used to handle the [onError] event. Its [call] method receives the exception emitted by the observable (line 29);
- line 31: we reuse the [showAlert] method from the previous example;
- line 33: we initiate the procedure to cancel the user’s request. This involves canceling all observables that are currently running;
- lines 35–41: the third parameter of type [Action0] is used to handle the [onCompleted] event. Its [call] method takes no parameters;
- line 39: the wait is canceled;
The [showInfo] method evolves as follows:
// [UiThread] annotation is unnecessary
protected void showInfo(int alea) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
if (!hasBeenCanceled) {
// one more piece of info
nbInfos++;
infoReponses.setText(String.format("List of responses (%s)", nbInfos));
// add the information to the list of answers
answers.add(0, String.valueOf(alea));
// display the responses
adapterAnswers.notifyDataSetChanged();
}
}
The method has two changes:
- line 1: we removed the AA annotation [@UiThread];
- we no longer count the responses to determine whether or not to stop waiting. It is now the observable’s [onCompleted] event that provides this information;
The [showAlert] method changes as follows:
// [UiThread] annotation is unnecessary
protected void showAlert(Throwable th) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception received");
}
if (!hasBeenCanceled) {
// cancel everything
doCancel();
// display it
new AlertDialog.Builder(activity).setTitle("Errors have occurred").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Close", null).show();
}
}
- The only change is in line 1: we removed the AA annotation [@UiThread];
Finally, the [doAnnuler] method changes as follows:
@Click(R.id.btn_Cancel)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Cancellation requested");
}
// memory
hasBeenCanceled = true;
// cancel asynchronous tasks
if (subscriptions != null) {
for (Subscription subscription : subscriptions) {
subscription.unsubscribe();
}
}
// end of wait
cancelWaiting();
}
- line 12: cancels a subscription and thus the observation of the associated process;
1.17.9. Execution
Launch the web service (section 1.16.1.7), launch the Android client, and repeat the tests you performed with the previous example (section 1.16.2.8).
1.17.10. Handling Cancellation
We repeat the same tests as for the previous example (section 1.16.2.9).
Test 1
We request 5 numbers even though the server has not been launched. We get the following logs:
After line 7, there are no more logs, which shows that the observer (Vue1Fragment) is no longer receiving notifications from the observed process.
Test 2
Now, let’s start the server and request 5 numbers with a 5-second delay, then click [Cancel] before the delay ends. The logs are as follows:
After line 6, there are no more logs, which shows that the observer (Vue1Fragment) is no longer receiving notifications from the observed process.
This is the expected behavior of a cancellation. We can therefore remove the boolean [hasBeenCanceled] from the [Vue1Fragment] code that we introduced in the previous example because the cancellation was not behaving as expected.
The fact that the observer no longer receives notifications after the observable is canceled does not mean that the HTTP requests themselves are canceled. To see this, we modify the [Dao] class as follows:
@Override
public Observable<Integer> getRandom(final int a, final int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getRandom [%s, %s] in progress", a, b));
}
// web client execution
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// wait
waitSomeTime(delay);
// synchronous HTTP call
Response<Integer> response = webClient.getAlea(a, b);
if (isDebugEnabled) {
try {
Log.d(String.format("%s", className), String.format("response [%s]", new ObjectMapper().writeValueAsString(response)));
} catch (JsonProcessingException e) {
Log.d(String.format("%s", className), "JSON deserialization error");
}
}
return response;
}
});
}
- lines 15–21: we log the result of the HTTP request from line 14;
The logs for test #2 are as follows:
- lines 1-5: the 5 requests were made;
- line 6: the user canceled;
- lines 7-11: we successfully receive the responses to the five HTTP requests. However, because the observable was canceled, these elements are not passed to the observer;
1.17.11. Conclusion
In the remainder of this document, client/server applications will be implemented using the RxAndroid library rather than the AA library for the following reasons:
- RxAndroid can be used in an Android application that does not use AA;
- RxAndroid does more than just facilitate asynchronous operations. It offers numerous methods for creating a new observable from another. These methods have no AA equivalent;
- As soon as one attempts to derive a class annotated by AA, such as a fragment, serious problems arise. One is then forced to abandon AA and use Solution 1 for asynchronous programming;
Readers interested in exploring the capabilities of the RxAndroid library further can consult the document [Introduction to RxJava. Application to Swing and Android Environments]. It uses RxAndroid without the AA library.
1.18. Example-17: Data Entry Components
We will create a new project to demonstrate some common components used in data entry forms.
1.18.1. Creating the project
We duplicate the [Example-13] project into [Example-17]:
![]() | ![]() |
The new project will have only one view [view1.xml]. Therefore, we will delete the view [view2.xml] and its associated fragment [View2Fragment] [2]. We will reflect this change in the fragment manager of [MainActivity]:
// our fragment manager, which must be redefined for each application
// must define the following methods: getItem, getCount, getPageTitle
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// the fragments
private final Fragment[] fragments = {new Vue1Fragment_()};
....
}
Rerun the project. It should display view #1 as before. We will work from this project.
1.18.2. The XML view of the form
![]() |
The view generated by the [vue1.xml] file is as follows:

The XML text of the view is as follows:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="30dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/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="30dp"
android:text="@string/view1_title"
android:textSize="30sp"/>
<Button
android:id="@+id/formButtonValidate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/TextViewFormCombo"
android:layout_below="@+id/TextViewFormCombo"
android:layout_marginTop="30dp"
android:text="@string/form_validate"/>
<TextView
android:id="@+id/textViewFormCheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormTitle"
android:layout_below="@+id/textViewFormTitle"
android:layout_marginTop="30dp"
android:text="@string/form_checkbox"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormCheckBox"
android:layout_below="@+id/textViewFormCheckBox"
android:layout_marginTop="30dp"
android:text="@string/form_radioButton"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormSeekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormRadioButton"
android:layout_below="@+id/textViewFormRadioButton"
android:layout_marginTop="30dp"
android:text="@string/form_seekBar"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireEdtText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireSeekBar"
android:layout_below="@+id/textViewFormulaireSeekBar"
android:layout_marginTop="30dp"
android:text="@string/input_form"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormBool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormEdtText"
android:layout_below="@+id/textViewFormEdtText"
android:layout_marginTop="30dp"
android:text="@string/boolean_form"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormDate"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:layout_alignLeft="@+id/textViewFormBool"
android:layout_below="@+id/textViewFormBool"
android:layout_marginTop="50dp"
android:gravity="center"
android:text="@string/form_date"
android:textSize="20sp"/>
<TextView
android:id="@+id/multilineFormTextView"
android:layout_width="150dp"
android:layout_height="100dp"
android:gravity="center"
android:layout_alignBaseline="@+id/textViewFormTitle"
android:layout_alignParentTop="true"
android:layout_marginLeft="400dp"
android:layout_toRightOf="@+id/textViewFormTitle"
android:text="@string/multiline_form"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormTime"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:gravity="center"
android:layout_alignLeft="@+id/textViewFormMultiline"
android:layout_below="@+id/textViewFormMultiline"
android:layout_marginTop="30dp"
android:text="@string/time_form"
android:textSize="20sp"/>
<TextView
android:id="@+id/TextViewFormCombo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormTime"
android:layout_below="@+id/textViewFormTime"
android:layout_marginTop="30dp"
android:text="@string/comboForm"
android:textSize="20sp"/>
<CheckBox
android:id="@+id/formCheckBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormCheckBox"
android:layout_marginLeft="100dp"
android:layout_toRightOf="@+id/textViewFormCheckBox"
android:text="@string/form_checkbox1"/>
<RadioGroup
android:id="@+id/formRadioGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireRadioButton"
android:layout_alignLeft="@+id/formCheckBox1"
android:orientation="horizontal">
<RadioButton
android:id="@+id/formRadioButton1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/form_radiobutton1"/>
<RadioButton
android:id="@+id/formRadioButton2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/form_radiobutton2"/>
<RadioButton
android:id="@+id/formRadioButton3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/form_radiobutton3"/>
</RadioGroup>
<SeekBar
android:id="@+id/formSeekBar"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
android:layout_alignLeft="@+id/formCheckBox1"/>
<EditText
android:id="@+id/formEditText1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireEdtText"
android:layout_alignLeft="@+id/formCheckBox1"
android:ems="10"
android:inputType="text">
</EditText>
<Switch
android:id="@+id/formSwitch1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormBool"
android:layout_alignLeft="@+id/formCheckBox1"
android:text="@string/form_switch"
android:textOff="No"
android:textOn="Yes"/>
<TimePicker
android:id="@+id/formTimePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormTime"
android:layout_alignLeft="@+id/multilineEditTextForm"
android:timePickerMode="spinner"
/>
<EditText
android:id="@+id/multilineEditTextForm"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_alignBaseline="@+id/multilineFormTextView"
android:layout_alignBottom="@+id/textViewMultiLineForm"
android:layout_marginLeft="50dp"
android:layout_toRightOf="@+id/multilineFormTextView"
android:ems="10"
android:inputType="textMultiLine">
</EditText>
<Spinner
android:id="@+id/formulaireDropDownList"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_alignBottom="@+id/TextViewComboForm"
android:layout_alignLeft="@+id/multilineEditTextForm">
</Spinner>
<DatePicker
android:id="@+id/DatePickerForm1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormDate"
android:layout_alignLeft="@+id/formCheckBox1"
android:datePickerMode="spinner"
android:calendarViewShown="false">
</DatePicker>
<TextView
android:id="@+id/textViewSeekBarValue"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormSeekBar"
android:layout_marginLeft="30dp"
android:layout_toRightOf="@+id/formSeekBar"
android:text=""/>
</RelativeLayout>
The main components of the form are as follows:
| |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
|
1.18.3. The form's strings
The form's strings are defined in the following [res/values/strings.xml] file:
![]() |
<resources>
<string name="app_name">Example-17</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- view 1 -->
<string name="view1_title">View #1</string>
<string name="form_checkbox">Checkboxes</string>
<string name="form_radioButton">Radio Buttons</string>
<string name="form_seekBar">Seek Bar</string>
<string name="form_input">Input field</string>
<string name="form_boolean">Boolean</string>
<string name="form_date">Date</string>
<string name="form_time">Time</string>
<string name="form_multiline">Multiline Input Field</string>
<string name="form_listview">List</string>
<string name="form_combo">Drop-down list</string>
<string name="form_checkbox1">1</string>
<string name="form_checkbox2">2</string>
<string name="form_radiobutton1">1</string>
<string name="form_radiobutton2">2</string>
<string name="form_radiobutton3">3</string>
<string name="form_switch"></string>
<string name="form_submit">Submit</string>
</resources>
1.18.4. The form fragment
![]() |
The [View1Fragment] class is as follows:
package examples.android.fragments;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.widget.*;
import android.widget.SeekBar.OnSeekBarChangeListener;
import examples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
// A fragment is a view displayed by a fragment container
@EFragment(R.layout.view1)
public class View1Fragment extends AbstractFragment {
// the fields of the view displayed by the fragment
@ViewById(R.id.formDropDownList)
Spinner dropDownList;
@ViewById(R.id.formButtonValidate)
Button buttonValidate;
@ViewById(R.id.formCheckBox1)
CheckBox checkBox1;
@ViewById(R.id.formRadioGroup)
RadioGroup radioGroup;
@ViewById(R.id.formSeekBar)
SeekBar seekBar;
@ViewById(R.id.form-EditText1)
EditText input;
@ViewById(R.id.formSwitch1)
Switch switch1;
@ViewById(R.id.formDatePicker1)
DatePicker datePicker1;
@ViewById(R.id.formTimePicker1)
TimePicker timePicker1;
@ViewById(R.id.form-multiline-edit-text)
EditText multiLines;
@ViewById(R.id.formRadioButton1)
RadioButton radioButton1;
@ViewById(R.id.formRadioButton2)
RadioButton radioButton2;
@ViewById(R.id.formRadioButton3)
RadioButton radioButton3;
@ViewById(R.id.textViewSeekBarValue)
TextView seekBarValue;
// 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");
}
@SuppressLint("DefaultLocale")
@Click(R.id.formButtonValidate)
protected void doValidate() {
...
}
@Override
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);
}
}
- lines 22–49: we retrieve the references for all components of the XML form [view1] (line 18);
- line 58: the [setChecked] method allows you to check a radio button or a checkbox;
- line 60: by default, the [DatePicker] component displays both a date input field and a calendar. Line 60 removes the calendar;
- line 62: [SeekBar].setMax() sets the maximum value of the slider. The minimum value is 0;
- lines 63–74: We handle the seek bar’s events. For every change made by the user, we want to display the slider’s value in the [TextView] on line 49;
- line 71: the [progress] parameter represents the slider’s value;
- lines 76–79: a list of [String]s that will be associated with the dropdown list;
- line 90: the fragment’s [updateFragment] method. When it is executed, the [activity] variable of the parent class has been initialized;
- line 92: the data source [list] is bound to the drop-down list adapter;
- lines 93–94: the [dataAdapter] is bound to the drop-down list [dropDownList];
- line 84: the [doValider] method is associated with a click on the [Valider] button;
The purpose of the [doValider] method is to display the values entered by the user. Its code is as follows:
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// list of messages to display
List<String> messages = new ArrayList<>();
// checkbox
boolean isChecked = checkBox1.isChecked();
messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
// radio buttons
int id = radioGroup.getCheckedRadioButtonId();
String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
// the SeekBar
int progress = seekBar.getProgress();
messages.add(String.format("SeekBar [value=%d]", progress));
// the input field
String text = String.valueOf(input.getText());
messages.add(String.format("Simple input [value=%s]", text));
// the switch
boolean state = switch1.isChecked();
messages.add(String.format("Switch [value=%s]", state));
// the date
int year = datePicker1.getYear();
int month = datePicker1.getMonth() + 1;
int day = datePicker1.getDayOfMonth();
messages.add(String.format("Date [%d, %d, %d]", day, month, year));
// multi-line text
String lines = String.valueOf(multiLines.getText());
messages.add(String.format("Multi-line input [value=%s]", lines));
// the time
int hour = timePicker1.getHour();
int minutes = timePicker1.getMinute();
messages.add(String.format("Time [%d, %d]", hour, minutes));
// dropdown list
int position = dropDownList.getSelectedItemPosition();
String selectedItem = String.valueOf(dropDownList.getSelectedItem());
messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
// display
doDisplay(messages);
}
- line 4: the entered values will be added to a list of messages;
- line 6: the [CheckBox].isChecked() method determines whether a checkbox is checked or not;
- line 9: the [RadioGroup].getCheckedButtonId() method returns the ID of the selected radio button or -1 if none is selected;
- line 10: the code [activity.findViewById(id)] retrieves the checked radio button and thus its label;
- line 13: the [SeekBar].getProgress() method returns the value of a slider;
- line 19: the method [Switch].isChecked() determines whether a switch is On (true) or Off (false);
- line 22: the [DatePicker].getYear() method retrieves the selected year using a [DatePicker] object;
- line 23: the [DatePicker].getMonth() method returns the selected month from a [DatePicker] object within the range [0,11];
- line 24: the [DatePicker].getDayOfMonth() method returns the selected day of the month using a [DatePicker] object within the range [1,31];
- line 30: the [TimePicker].getHour() method returns the selected hour using a [TimePicker] object;
- line 31: the [TimePicker].getMinute() method returns the selected minutes using a [TimePicker] object;
- line 34: the [Spinner].getSelectedItemPosition() method returns the position of the selected item in a drop-down list;
- line 35: the [Spinner].getSelectedItem() method returns the selected item in a drop-down list;
The [doAfficher] method, which displays the list of entered values, is as follows:
private void displayMessages(List<String> messages) {
// Build the text to be displayed
StringBuilder text = new StringBuilder();
for (String message : messages) {
text.append(String.format("%s\n", message));
}
// display it
new AlertDialog.Builder(activity).setTitle("Entered values").setMessage(text).setNeutralButton("Close", null).show();
}
- line 1: the method receives a list of messages to display;
- lines 3–6: a [StringBuilder] object is constructed from these messages. For string concatenation, the [StringBuilder] type is more efficient than the [String] type;
- line 8: a dialog box displays the text from line 3:

1.18.5. Running the project
Run the project and test the various input components.
1.19. Example-18: Using a view pattern
1.19.1. Creating the project
We create a new project [Example-18] by copying the [Example-13] project.
![]() | ![]() |
1.19.2. The view template
We want to reuse the two views from the project and include them in a template:
![]() |

Each of the two views will be structured the same way:
- in [1], a header;
- in [2], a left column that could contain links;
- in [3], a footer;
- in [4], content.
This is achieved by modifying the activity’s base view [activity_main.xml];
![]() | ![]() |
The XML code for the [main] view 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>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_marginTop="75dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="0.1"
android:background="@color/lavenderblushh2">
<TextView
android:id="@+id/textViewHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:text="@string/txt_header"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="0.8"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/left"
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="@color/lightcyan2">
<TextView
android:id="@+id/txt_left"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:text="@string/txt_left"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
<examples.android.architecture.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/floral_white"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</LinearLayout>
<LinearLayout
android:id="@+id/bottom"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="0.1"
android:background="@color/wheat1">
<TextView
android:id="@+id/textViewBottom"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:text="@string/txt_bottom"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
- The header [1] is generated by lines 38–54;
- the left panel [2] is generated by lines 56–84;
- the footer [3] is created by lines 86–101;
- the content [4] is generated by lines 78–84;
The [main] XML view uses information found in the [res/values/colors.xml] and [res/values/strings.xml] files:
![]() |
The [colors.xml] file is as follows:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="red">#FF0000</color>
<color name="blue">#0000FF</color>
<color name="wheat">#FFEFD5</color>
<color name="floral_white">#FFFAF0</color>
<color name="lavenderblushh2">#EEE0E5</color>
<color name="lightcyan2">#D1EEEE</color>
<color name="wheat1">#FFE7BA</color>
</resources>
and the following [strings.xml] file:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">example-12</string>
<string name="action_settings">Settings</string>
<string name="view1_title">View #1</string>
<string name="textView_name">What is your name:</string>
<string name="btn_Validate">Validate</string>
<string name="btn_view2">View #2</string>
<string name="view2_title">View #2</string>
<string name="btn_view1">View #1</string>
<string name="textView_hello">"Hello "</string>
<string name="txt_header">Header</string>
<string name="txt_left">Left</string>
<string name="txt_bottom">Bottom</string>
</resources>
Create a runtime context for this project and run it.
1.20. Example-19: The [ListView] Component
The [ListView] component allows you to repeat a specific view for each item in a list. The repeated view can be of any complexity, ranging from a simple string to a view that allows you to enter information for each item in the list. We will create the following [ListView]:

Each view in the list has three components:
- an [TextView] for information;
- a [CheckBox];
- a clickable [TextView];
1.20.1. Creating the project
We create a new project [Example-19] by cloning the [Example-18] project.
![]() | ![]() |
![]() |
We will develop the project as described in [3].
1.20.2. The session
![]() |
The session stores data shared between the activity and fragments:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// a list of data
private List<Data> list = new ArrayList<>();
// getters and setters
...
}
- line 11: the data list used by both views;
The [Data] class is as follows:
package examples.android.architecture;
public class Data {
// data
private String text;
private boolean isChecked;
// constructor
public Data(String text, boolean isChecked) {
this.text = text;
this.isChecked = isChecked;
}
// getters and setters
...
}
- line 6: the text that will populate the first [TextView] of each list item;
- line 7: the boolean that will be used to check or uncheck the [checkBox] for each item in the list;
1.20.3. The [MainActivity]
The code for the [@AfterInject] method becomes the following:
// session injection
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// create a list of data
List<Data> list = session.getList();
for (int i = 0; i < 20; i++) {
list.add(new Data("Text # " + i, false));
}
}
- lines 12–15: initialization of the list of data present in the session;
1.20.4. The initial [View1] view
![]() | ![]() |
The XML view [view1.xml] displays the area [1] above. Its code 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:id="@+id/textView_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:text="@string/view1_title"
android:textSize="50sp" />
<Button
android:id="@+id/button_view2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textView_title"
android:layout_below="@+id/textView_title"
android:layout_marginTop="50dp"
android:text="@string/btn_vue2" />
<ListView
android:id="@+id/listView1"
android:layout_width="600dp"
android:layout_height="200dp"
android:layout_alignParentLeft="true"
android:layout_below="@+id/button_vue2"
android:layout_marginLeft="30dp"
android:layout_marginTop="50dp" >
</ListView>
</RelativeLayout>
- lines 7–16: the [TextView] component [2];
- lines 27–35: the [ListView] component [4];
- lines 18–25: the [Button] component [3];
1.20.5. The view repeated by the [ListView]
![]() | ![]() |
The view repeated by the [ListView] is the following [list_data] view:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/wheat" >
<TextView
android:id="@+id/txt_Label"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:text="@string/txt_dummy" />
<CheckBox
android:id="@+id/checkBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/txt_Label"
android:layout_marginLeft="37dp"
android:layout_toRightOf="@+id/txt_Label"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/textViewRemove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Label"
android:layout_alignBottom="@+id/txt_Label"
android:layout_marginLeft="68dp"
android:layout_toRightOf="@+id/checkBox1"
android:text="@string/txt_remove"
android:textColor="@color/blue"
android:textSize="20sp" />
</RelativeLayout>
- lines 8–14: the [TextView] component [1];
- lines 16–23: the [CheckBox] component [2];
- lines 25-35: the [TextView] component [3];
1.20.6. The [Vue1Fragment] fragment
![]() |
The [Vue1Fragment] fragment manages the [vue1] XML view. Its code is as follows:
package exemples.android.fragments;
import android.view.View;
import android.widget.ListView;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import examples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// fields of the view displayed by the fragment
@ViewById(R.id.listView1)
protected ListView listView;
// the list adapter
private ListAdapter adapter;
// initialization complete
private boolean initDone = false;
@AfterViews
void afterViews() {
// memory
afterViewsDone = true;
}
@Click(R.id.button_vue2)
void navigateToView2() {
// navigate to view 2
mainActivity.navigateToView(1);
}
public void remove(int position) {
...
}
@Override
protected void updateFragment() {
if (!initDone) {
// bind data to the [ListView]
adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
initDone = true;
}
// In case the fragment has been (re)generated—in this case, the ListView must be re-linked to its adapter
listView.setAdapter(adapter);
// if other fragments have changed the data source - in this case, the ListView must be refreshed
adapter.notifyDataSetChanged();
}
}
- line 15: the XML view [view1] is associated with the fragment;
- lines 26–30: the [@AfterViews] method does nothing. However, it is necessary to set the [afterViewsDone] variable to true because it is used by the parent class [AbstractFragment];
- lines 42–53: the [updateFragment] method, which is called every time the fragment becomes visible. The method was written here as if the fragment could leave the adjacency of the displayed fragment and thus reset its lifecycle. This is not the case here, but it would be if the application were to have 3 fragments with an adjacency of 1;
- line 44: the [ListView] adapter only needs to be initialized once;
- line 46: we associate a [ListAdapter] with this [ListView]. We will build this class. It derives from the [ArrayAdapter] class, which we have already used to associate data with a [ListView]. We pass various pieces of information to the [ListAdapter] constructor:
- a reference to the current activity,
- the identifier of the view that will be instantiated for each item in the list,
- a data source to populate the list,
- a reference to the fragment. This will be used to handle a click on a [Remove] link in the [ListView] via the [doRemove] method on line 38;
- Line 50: The adapter is bound to the [ListView]. At the same time, the [lists] data source is bound to the [ListView]. This operation is performed here every time view #1 is displayed. In reality, it only needs to be done once the [@AfterViews] method has been executed. Here, the statement is executed too often. We need a boolean variable that would tell us that the [@AfterViews] method has just been executed and that the [ListView] must therefore be reassociated with its adapter;
- Line 52: We refresh the [ListView]. In this example, this serves no purpose because only View #1 can modify the [ListView]’s data source. Let’s consider a more general case where view #2 could also change the [ListView]’s data source. We’ll encounter such examples later in this document. In this case, when switching from view #2 to view #1, the [ListView] in view #1 must be refreshed;
1.20.7. The [ListAdapter] of the [ListView]
![]() | ![]() |
The [ListAdapter] class
- configures the data source of the [ListView];
- manages the display of the various elements in the [ListView];
- handles the events of these elements;
Its code is as follows:
package exemples.android.fragments;
import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {
// the runtime context
private Context context;
// the ID of the layout for a row in the list
private int layoutResourceId;
// the list data
private List<Data> data;
// the fragment that displays the [ListView]
private Vue1Fragment fragment;
// the adapter
final ListAdapter adapter = this;
// constructor
public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
super(context, layoutResourceId, data);
// store the info
this.context = context;
this.layoutResourceId = layoutResourceId;
this.data = data;
this.fragment = fragment;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
}
- line 5: the [ListAdapter] class extends the [ArrayAdapter] class;
- line 19: the constructor;
- line 20: don’t forget to call the constructor of the parent class [ArrayAdapter] with the first three parameters;
- lines 22–25: we store the constructor’s information;
- line 29: the [getView] method will be called repeatedly by the [ListView] to generate the view for element #[position]. The returned [View] result is a reference to the created view.
The code for the [getView] method is as follows:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// create the current row of the ListView
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// the text
TextView textView = (TextView) row.findViewById(R.id.txt_Label);
textView.setText(data.get(position).getText());
// the checkbox
CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
checkBox.setChecked(data.get(position).isChecked());
// the [Remove] link
TextView txtRemove = (TextView) row.findViewById(R.id.textViewRemove);
txtRemove.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
fragment.doRemove(position);
}
});
// Handle the click on the checkbox
checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
data.get(position).setChecked(isChecked);
}
});
// return the row
return row;
}
- Line 2: The method takes three parameters. We will only use the first one;
- line 4: we create the view for element #[position]. This is the [list_data] view whose ID was passed as the second parameter to the constructor. Then we retrieve the references to the components of the view we just instantiated;
- line 6: we retrieve the reference to [TextView] #1;
- line 7: we assign it text from the data source that was passed as the third parameter to the constructor;
- line 9: we retrieve the reference to [CheckBox] #2;
- line 10: we check or uncheck it using a value from the [ListView]'s data source;
- line 12: retrieve the reference to [TextView] #3;
- lines 13–18: handle the click on the [Remove] link;
- line 16: the [Vue1Fragment].doRetirer method handles this click. It makes more sense to have the fragment displaying the [ListView] handle this event. It has an overview that the [ListAdapter] class does not have. The reference to the [Vue1Fragment] fragment was passed as the fourth parameter to the class constructor;
- Lines 20–25: Handle the click on the checkbox. The action performed on it is reflected in the data it displays. This is for the following reason: The [ListView] is a list that displays only a portion of its items. Thus, a list item is sometimes hidden and sometimes displayed. When element #i needs to be displayed, the [getView] method from line 2 above is called for position #i. Line 10 will recalculate the state of the checkbox based on the data it is linked to. Therefore, it must store the state of the checkbox over time;
1.20.8. Removing an item from the list
Clicking the [Remove] link is handled in the [Vue1Fragment] fragment by the following [doRetirer] method:
public void doRemove(int position) {
// Remove element #[position] from the list
List<Data> list = mainActivity.getList();
list.remove(position);
// note the scroll position to return to it
// read
// [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// position of the first element, whether fully visible or not
int firstPosition = listView.getFirstVisiblePosition();
// Y offset of this element relative to the top of the ListView
// measures the height of the potentially hidden portion
View v = listView.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// refresh the [ListView]
adapter.notifyDataSetChanged();
// move to the correct position in the ListView
listView.setSelectionFromTop(firstPosition, top);
}
- line 1: Get the position in the [ListView] of the [Remove] link that was clicked;
- line 3: retrieve the data list;
- line 4: remove the item at position [position];
- line 15: we refresh the [ListView]. Without this, nothing changes visually.
- Lines 5–13, 17: a rather complex process. Without it, the following happens:
- the [ListView] displays lines 15–18 of the data list,
- line 16 is deleted,
- line 15 above resets it completely, and the [ListView] then displays lines 0–3 of the data list;
With the lines above, the deletion occurs and the [ListView] remains positioned on the line following the deleted line.
1.20.9. The XML view [View2]
![]() | ![]() |
The XML code for the view 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:id="@+id/textView_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:text="@string/view2_title"
android:textSize="50sp" />
<Button
android:id="@+id/button_view1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textViewResultats"
android:layout_marginTop="25dp"
android:layout_alignLeft="@+id/textView_title"
android:text="@string/btn_view1" />
<TextView
android:id="@+id/textViewResultats"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textView_title"
android:layout_marginTop="50dp"
android:layout_alignLeft="@+id/textView_title"
android:text="" />
</RelativeLayout>
- lines 6–15: [TextView] component #1;
- lines 26–33: [TextView] component #2;
- lines 17-24: [Button] component #3;
1.20.10. The [Vue2Fragment] fragment
![]() | 123 ![]() |
The [Vue2Fragment] fragment manages the [vue2] XML view. Its code is as follows:
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
// view fields
@ViewById(R.id.textViewResultats)
TextView txtResults;
@AfterViews
void initFragment(){
// memory
afterViewsDone = true;
}
@Click(R.id.button_view1)
void navigateToView1() {
// navigate to view 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// display the items in the list that were selected in View 1
StringBuilder text = new StringBuilder("Selected items [");
for (Data data : mainActivity.getList()) {
if (data.isChecked()) {
text.append(String.format("(%s)", data.getText()));
}
}
text.append("]");
txtResults.setText(text);
}
}
The important code is in the [updateFragment] method on line 32:
- line 34: we calculate the text to be displayed in [TextView] #2;
- lines 35–39: we iterate through the list of data displayed by the [ListView]. It is stored in the activity;
- line 36: if data item i has been checked, the associated label is added to a [StringBuilder];
- line 41: the [TextView] displays the calculated text;
1.20.11. Execution
Create a run configuration for this project and run it.
1.20.12. Improvement
In the previous example, we used a List<Data> data source where the [Data] class was as follows:
package examples.android.fragments;
public class Data {
// data
private String text;
private boolean isChecked;
// constructor
public Data(String text, boolean isChecked) {
this.text = text;
this.isChecked = isChecked;
}
...
}
In Line 7, we used a Boolean variable to manage the checkboxes for the items in the [ListView]. Often, the [ListView] needs to display data that can be selected by checking a box, even though the item in the data source does not have a Boolean field corresponding to that box. In that case, you can proceed as follows:
The [Data] class becomes the following:
package exemples.android.fragments;
public class Data {
// data
private String text;
// constructor
public Data(String text) {
this.text = text;
}
// getters and setters
...
}
We create a [CheckedData] class derived from the previous one:
package examples.android.fragments;
public class CheckedData extends Data {
// checked element
private boolean isChecked;
// constructor
public CheckedData(String text, boolean isChecked) {
// parent
super(text);
// local
this.isChecked = isChecked;
}
// getters and setters
...
}
Then simply replace the [Data] type with the [CheckedData] type throughout the code (MainActivity, ListAdapter, View1Fragment, View2Fragment). For example, in [MainActivity]:
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// create a list of data
List<CheckedData> list = session.getList();
for (int i = 0; i < 20; i++) {
list.add(new CheckedData("Text # " + i, false));
}
}
The project for this version is provided under the name [Example-19B].
1.21. Example-20: Using a menu
1.21.1. Creating the project
We duplicate the [Example-19B] project into the [Example-20] project:
![]() | ![]() |
![]() | 3 ![]() |
We will remove the buttons from views 1 and 2 and replace them with menu options [1-2].
1.21.2. The XML definition of the menus
![]() |
The file [res/menu/menu_vue1] defines the menu for view #1:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuOptions"
app:showAsAction="ifRoom"
android:title="@string/menuOptions">
<menu>
<item
android:id="@+id/actionHideShowAll"
android:title="@string/actionHideShowAll"/>
<item
android:id="@+id/actionHideShowActions"
android:title="@string/actionHideShowActions"/>
<item
android:id="@+id/actionHideShowActionsValidate"
android:title="@string/actionHideShowActionsValidate"/>
</menu>
</item>
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationView2"
android:title="@string/navigationView2"/>
</menu>
</item>
</menu>
Menu items are defined by the following information:
- android:id: the element's identifier;
- android:title: the item's label;
- app:showsAsAction: indicates whether the menu item can be placed in the activity's action bar. [ifRoom] indicates that the item should be placed in the action bar if there is room for it;
- a menu option can itself be a submenu (the <menu> tag, lines 25, 29);
The file [res / menu / menu_vue2] defines the menu for view #2:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationView1"
android:title="@string/navigationView1"/>
</menu>
</item>
</menu>
1.21.3. Menu management in the abstract class [AbstractFragment]
We will factor out menu management into the parent class [AbstractFragment] of the two views:
package exemples.android.architecture;
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractFragment extends Fragment {
// data accessible to child classes
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
protected String className;
// activity
protected IMainActivity mainActivity;
protected Activity activity;
// session
protected Session session;
// menu
private Menu menu;
private int[] menuOptions;
private boolean initDone;
// constructor
public AbstractFragment() {
// initialization
className = getClass().getSimpleName();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// memory
this.menu = menu;
// log
if (isDebugEnabled) {
Log.d(className, String.format("creating menu"));
}
// retrieve the menu options if this hasn't already been done
if (!initDone) {
// Retrieve the menu options
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// Transfer the list of options to an array
menuOptions = new int[menuOptionsIds.size()];
for (int i = 0; i < menuOptions.length; i++) {
menuOptions[i] = menuOptionsIds.get(i);
}
// activity
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
// memory
initDone = true;
}
// we ask the child fragment to update
updateFragment();
}
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
...
}
// display menu options -----------------------------------
protected void setAllMenuOptions(boolean isVisible) {
....
}
protected void setMenuOptions(MenuItemState[] menuItemStates) {
...
}
// update child class
protected abstract void updateFragment();
}
- line 42: the logs show that the [onCreateOptionsMenu] method is called every time the fragment is displayed. It is called very late, specifically after the [updateFragment] method has been called. This suggests that it could be used to update the fragment. That is what we will do here (line 63);
- line 42: the method has two parameters:
- [menu]: which is an empty menu;
- [inflater]: a tool that allows us to create the menu from its initial description. We won’t use this option here because we’ll use an AA annotation that will do it for us;
- line 44: we store the menu. We will need it later;
- lines 52–53: we store the IDs of all menu items in the array from line 28;
- lines 55–57: the logs show that when the [onCreateOptionsMenu] method is called, the [Fragment.getActivity()] method returns the activity associated with the fragment;
- line 55: we store the activity as an instance of the Android [Activity] class;
- line 56: we store the activity as an instance of the [IMainActivity] interface;
- line 57: we store the session;
- line 59: we note that the class has already been initialized so we don’t have to do it again (line 50);
- line 63: we ask the child fragment to update itself. This is possible because the fragment is both visible and associated with its view and its menu;
The [getMenuOptions] method, which retrieves the IDs of menu items, is as follows:
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);
}
}
}
The [setAllMenuOptions] method allows you to hide or show all menu options;
protected void setAllMenuOptions(boolean isVisible) {
// update all menu options
for (int menuItemId : menuOptions) {
menu.findItem(menuItemId).setVisible(isVisible);
}
}
The [setMenuOptions] method allows you to hide or show certain menu options;
protected void setMenuOptions(MenuItemState[] menuItemStates) {
// update certain menu options
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
The [MenuItemState] class is as follows:
![]() |
package exemples.android.architecture;
public class MenuItemState {
// menu option 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
...
}
1.21.4. Menu management in the [View1Fragment] fragment
The [Vue1Fragment] class becomes the following:
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {
...
@OptionsItem(R.id.navigationVue2)
void navigateToView2() {
// navigate to View 2
mainActivity.navigateToView(1);
}
@OptionsItem(R.id.actionValider)
void validate() {
// display a message
Toast.makeText(activity, "Validate", Toast.LENGTH_SHORT).show();
}
private boolean hideShowAll = true;
@OptionsItem(R.id.actionHideShowAll)
void hideShowAll() {
// change state
actionHideShowAll = !actionHideShowAll;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuNavigation, actionHideShowAll), new MenuItemState(R.id.menuActions, actionHideShowAll)});
}
private boolean actionHideShowActions = true;
@OptionsItem(R.id.actionHideShowActions)
void actionHideShowActions() {
// change state
actionHideShowActions = !actionHideShowActions;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionHideShowActions)});
}
private boolean actionHideShowActionsValid = true;
@OptionsItem(R.id.actionHideShowActionsValidate)
void actionHideShowActionsValidate() {
// change state
actionHideShowActionsValidate = !actionHideShowActionsValidate;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionHideShowActionsValidate)});
}
...
@Override
protected void updateFragment() {
....
// update the menu
//setMenuOptions(...)
}
}
- line 2: the menu [res/menu/menu_vue1.xml] is associated with the fragment;
- line 48: when the [updateFragment] method is executed, the menu can also be updated to reflect the fragment's new state;
- line 7: the annotation [@OptionsItem(R.id.navigationVue2)] annotates the method that must be executed when the menu option [Navigation / View 2] is clicked;
- lines 19–25: to hide a branch of the menu, simply hide its root option;
- line 24: the root options [menuNavigation, menuActions] are shown or hidden;
- line 40: to show an option in a menu branch, you must not only show that option but also all the options encountered when moving from the leaf option back up to the menu root;
1.21.5. Menu management in the [Vue2Fragment] fragment
Similar code can be found in the fragment of View 2:
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import examples.android.models.CheckedData;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue2)
@OptionsMenu(R.menu.menu_vue2)
public class View2Fragment extends AbstractFragment {
// view fields
@ViewById(R.id.textViewResultats)
TextView txtResults;
@OptionsItem(R.id.navigationVue1)
void navigateToView1() {
// navigate to view 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// display the items in the list that were selected in View 1
StringBuilder text = new StringBuilder("Selected items [");
for (CheckedData data : session.getList()) {
if (data.isChecked()) {
text.append(String.format("(%s)", data.getText()));
}
}
text.append("]");
txtResults.setText(text);
// update the menu
// setMenuOptions(...)
}
}
- line 35: display the [Navigation / View 1] option;
- lines 17-20: when the [Navigation / View 1] option is clicked, the [navigateToView1] method is called;
1.21.6. Execution
Create a runtime context for this project and run it.
1.22. Example-21: Refactoring the [AbstractFragment] class
The previous example showed us that when the fragment has a menu, its [onCreateOptionsMenu] method is a good place to ask the fragment to update itself:
- it is called exactly once when the fragment is about to be displayed;
- when it is called, the fragment’s associations with its activity, view, and menu are established;
To demonstrate this, we’ll revisit Example 12, which features many fragments whose adjacency can be modified. In that example, the fragments did not have a menu. We’ll associate an empty menu with them.
1.22.1. Creating the project
We duplicate the [Example-12] project into the [Example-21] project:
![]() | ![]() |
1.22.2. The fragment menu
![]() |
The menu added for the fragments will be empty:
<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">
</menu>
What you need to understand here is that the activity already has its own menu [menu_main]:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="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>
When an activity already has a menu, the menu associated with the fragments is added to the activity's menu: you therefore have the options from both menus. Here, the fragments' menu will be empty. So you will only see the activity's menu.
1.22.3. The Fragments
![]() |
We reuse the abstract class [AbstractFragment] from the previous example (see section 1.21.3). We associate the menu [menu_fragment] with the two fragments:
@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class View1Fragment extends AbstractFragment {
In both fragments [PlaceholderFragment] and [Vue1Fragment], we remove any references to the old abstract class [AbstractFragment].
1.22.4. Execution
Run the app and verify that it works. Check the logs to see when the [onCreateOptionsMenu] method of the [AbstractFragment] class is executed. It is now this method that calls the [updateFragment] method of the child fragments.
1.23. Example-22: Saving/Restoring the State of the Activity and Fragments
1.23.1. The Problem
Here we address the issue of rotating the Android device (portrait <--> landscape). To illustrate this, we’ll revisit the previous Example 21:

If we rotate the device [1], we get the following new view:

We can see that:
- in [1], the [Fragment #3] tab has disappeared;
- in [2], the text displayed is indeed that of Fragment No. 3, but the visit counter is incorrect;
During this rotation, the logs are as follows:
- line 1: we can see that the activity is completely rebuilt;
- lines 3–7: the same applies to the five fragments managed by the activity;
- line 21: fragment #3 is about to be displayed. We see that before incrementing, the visit count is 0;
We can then explain the result obtained after rotation as follows:
- the [MainActivity] class initially creates a tab bar with a single tab labeled [View 1]. This is the tab that is visible;
- After the device rotates, the page manager [mViewPager] re-displays the same fragment, which in this case is fragment #3. It is important to remember here that tabs and fragments are different concepts and have different lifecycles. The [updateFragment] method of fragment #3 will execute:
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// increment visit count
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
textViewInfo.setText(String.format("%s, visit %s", text, numVisit));
}
- Line 7: The last visit ID is read from the session. However, the session—like everything else—has been reset, and the visit ID has been reset to zero. This explains the result displayed in fragment #3;
1.23.2. Methods for saving/restoring the activity and fragments
1.23.2.1. Solution 1: Manual backup
When the device rotates, two methods of the activity are called:
// Activity save/restore management ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// parent
super.onSaveInstanceState(outState);
// Save activity state
// ....
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// restore the activity
// ...
}
- Lines 2–8: The [onSaveInstanceState] method is called by the system during rotation. This is where the activity can be saved. If nothing is done, nothing is saved. The activity state must be saved in the [Bundle outState] parameter passed to the method. The [Bundle] class resembles a dictionary. It has methods [putString, putInt, putLong, putBoolean, putChar, ...] with two parameters: void putT(String key, T value);
- Lines 10–16: The [onCreate] method is called when the activity is created. If the activity’s state has been saved, this saved state is passed to it in the [Bundle savedInstanceState] parameter. To retrieve the saved values, methods such as [getString, getInt, getLong, getBoolean, getChar, ...] with a single parameter are available: T getT(String key);
Fragments have these same two methods to save their state.
We will use this information to save and restore the state of Example 21. To do this, we duplicate the [Example-21] project into [Example-22].
1.23.2.2. Solution 2: Automatic saving
The Android documentation states that when the device is rotated, you can prevent a fragment from being destroyed by using the statement: [Fragment].setRetainInstance(true). Several articles on [StackOverflow] recommend using this instruction only for fragments without a visual interface [http://stackoverflow.com/questions/11182180/understanding-fragments-setretaininstanceboolean, http://stackoverflow.com/questions/12640316/further-understanding-setretaininstancetrue, http://stackoverflow.com/questions/21203948/setretaininstancetrue-in-oncreate-fragment-in-android]. I tested this statement on two examples: Example-17 (Section 1.18—a single-fragment application that displays a form) and Example-21 (Section 1.22—a five-fragment application). In both cases, applying this single instruction to all fragments of the application proved insufficient to correctly restore the view displayed when the device was rotated. Rather than building two models, one based on [setRetainInstance(true)] and another based on [setRetainInstance(false)]—which is the default value—I decided to follow the recommendations from [StackOverflow] and keep the default value of false for the [setRetainInstance(boolean)] method. The statement: [Fragment].setRetainInstance(true) has never been used in the rest of this document.
1.23.3. The backup/restore method for the [Example-22] project
The [Example-22] project evolves as follows:
![]() |
Two new classes appear:
- [PlaceHolderFragmentState], which will store the state of a fragment of type [PlaceHolderFragment];
- [Vue1FragmentState], which will store the state of a fragment of type [Vue1Fragment];
These classes are as follows:
package exemples.android;
public class Vue1FragmentState {
// Vue1Fragment state
private boolean hasBeenVisited = false;
// getters and setters
...
}
- line 5: the boolean [hasBeenVisited] is true if the fragment [Vue1Fragment] has been visited (displayed) at least once. This field was created for the example because the fragment [Vue1Fragment] has nothing to save;
The [PlaceHolderFragmentState] class is as follows:
package exemples.android;
public class PlaceHolderFragmentState {
// visited or not
private boolean hasBeenVisited;
// displayed text
private String text;
// getters and setters
...
}
- line 5: we see the boolean [hasBeenVisited];
- line 7: the text displayed by the fragment at the moment it needs to be saved. We saw that this text was lost during rotation;
The state of the fragments will be stored in the session, and the activity will be responsible for saving and restoring this session. The session evolves as follows:
package exemples.android;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// number of fragments visited
private int numVisit;
// number of [PlaceholderFragment] fragments displayed in the second tab
private int numFragment = -1;
// selected tab number
private int selectedTab = 0;
// current view number
private int currentView;
// fragment saves ---------------
private Vue1FragmentState vue1FragmentState;
private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];
// constructor
public Session() {
for (int i = 0; i < placeHolderFragmentStates.length; i++) {
placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
}
vue1FragmentState = new Vue1FragmentState();
}
// getters and setters
...
}
- line 18: the state of the [Vue1Fragment] fragment;
- line 19: the state of fragments of type [PlaceHolderFragment];
- lines 22–27: in the session constructor, the fields from lines 18 and 19 are initialized;
- lines 12–15: two new fields appear:
- line 13: the number of the last selected tab;
- line 15: the number of the last fragment displayed;
The activity saves/restores the session as follows:
// Activity save/restore management ----------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// parent
super.onSaveInstanceState(outState);
// save session
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// log
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
// retrieve 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();
}
}
}
}
- line 8: the session is saved as a JSON string;
- line 29: restore the session from its JSON string;
To manage the saving and restoring of fragments, the abstract class [AbstractFragment] evolves as follows:
// Save/restore management -----------------------------------------------
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// Save?
if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
// the fragment is about to be hidden - save it
saveFragment();
saveFragmentDone = true;
}
// memory
this.isVisibleToUser = isVisibleToUser;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// parent
super.onActivityCreated(savedInstanceState);
// log
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// the fragment must be restored
fragmentHasToBeInitialized = true;
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// log
if (isDebugEnabled) {
Log.d(className, "onSaveInstanceState");
}
// parent
super.onSaveInstanceState(outState);
// Save the fragment only if it is visible
if (isVisibleToUser && !saveFragmentDone) {
saveFragment();
saveFragmentDone = true;
}
}
// child classes
protected abstract void updateFragment();
protected abstract void saveFragment();
- We decide to save the state of the fragments in the session at two points:
- lines 2–14: when the fragment changes from visible to hidden;
- lines 29–42: when the system indicates that the fragment should be saved and the fragment is visible (line 38);
This mechanism prevents saving more often than necessary. Indeed, since we saved the state of fragment i when it changed from visible to hidden, when fragment j is displayed and a rotation occurs, there is no need to save fragment i again. If it has not been redisplayed since its last save, then its state has not changed. Only the state of fragment j needs to be saved. This mechanism also has another advantage: it is not only during a device rotation that we need to save a fragment’s state. There is also the case of pure navigation between fragments, for example in a tabbed system. In such cases, we want to retrieve a fragment in the state it was in when it was last displayed. This state may have partially disappeared if the fragment was at some point removed from the vicinity of the displayed fragments. The fragment is not then fully reconstructed, but its associated view is. The save performed when the fragment became hidden will be used to restore the last state of this view;
- lines 10, 40: to avoid making two successive saves, the boolean [saveFragmentDone] is used to indicate that a save has been made;
- lines 9, 39: the child fragment is asked to save its state. The [saveFragment] method is abstract (line 47). It is therefore up to the child classes to implement it;
- lines 16–26: the [onActivityCreated] method is used to set the boolean [fragmentHasToBeInitialized] to true. This is because the child fragment needs to know that it must fully reinitialize the fragment’s state from a state it will find in the session;
Still in the [AbstractFragment] class, the [onCreateOptionsMenu] method changes as follows:
// Update the fragment
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// memory
this.menu = menu;
// log
if (isDebugEnabled) {
Log.d(className, String.format("creating menu"));
}
...
// ask the child fragment to update
updateFragment();
// save to be performed
saveFragmentDone = false;
}
- line 14: we saw that the boolean [saveFragmentDone] was set to true when a save was performed. At some point, it must be reset to false. When the [updateFragment] method (line 12) of the child fragment is executed, it becomes visible. However, it is when a fragment is visible that it must be saved, specifically at the moment it transitions from the visible state to the hidden state. We then set the boolean [saveFragmentDone] to false so that the save can take place;
1.23.4. Saving the [Vue1Fragment] fragment
Fragments are saved in the [saveFragment] method called by the parent class [AbstractFragment]:
// save fragment state
@Override
public void saveFragment() {
// log
if (isDebugEnabled) {
Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
}
// Save the fragment's state to the session
Vue1FragmentState state = new Vue1FragmentState();
state.setHasBeenVisited(true);
session.setVue1FragmentState(state);
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- lines 9–11: saving the fragment’s state in the session. When the [saveFragment] method is called, the fragment is visible. Therefore, the boolean [hasBeenVisited] must be set to true (line 10);
1.23.5. Saving the [PlaceHolderFragment] fragment
Fragments are saved in the [saveFragment] method called by the parent class [AbstractFragment]:
@Override
public void saveFragment() {
// Save the fragment's state to the session
PlaceHolderFragmentState state = new PlaceHolderFragmentState();
state.setText(textViewInfo.getText().toString());
state.setHasBeenVisited(true);
session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- lines 4–7: save the fragment’s state to the session;
- line 5: the text currently displayed by the [TextView] textViewInfo is saved;
- line 6: the fragment's [hasBeenVisited] boolean is set to true;
- line 7: the fragment's state is saved in the session in the [placeHolderFragmentStates] array. The index of the element to initialize is the fragment's section number minus one;
1.23.6. Restoring the [Vue1Fragment] fragment
Fragments are restored in the [updateFragment] method:
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
}
// restore?
if (fragmentHasToBeInitialized) {
// restore state
hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
fragmentHasToBeInitialized = false;
}
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
}
// navigation?
boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
if (navigation) {
// increment visit count
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// display the visit number
Toast.makeText(activity, String.format("Visit #%s", numVisit), Toast.LENGTH_SHORT).show();
}
// change current view number
session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
}
- Lines 8–12: Restoring the fragment’s state. The boolean [fragmentHasToBeInitialized] was initialized by the parent class [AbstractFragment]. When it is true, the fragment has just been reconstructed and must be reinitialized. This is where that happens. In this specific example, there is nothing to do. We simply showed that we could retrieve the value of the boolean [hasBeenVisited] from the fragment’s saved state (line 10);
- line 11: don’t forget to set [fragmentHasToBeInitialized] back to false, so that when we return to this fragment later without the device having rotated, we don’t perform an unnecessary initialization of the fragment;
- lines 18–26: increment the visit counter. Here, there is a challenge: when restoring the fragment, we do not want to increment this counter. We need to distinguish here between:
- simple navigation that brings the user back to the [View 1] tab;
- a restore when the user rotates their device while the [View 1] tab is displayed;
We distinguish between these two cases using the view number stored in the session. This number is that of the last view displayed (line 28).
- line 18: navigation occurs rather than a refresh if the number of the last view differs from that of the current view;
- lines 21–25: incrementing the visit counter and displaying it;
1.23.7. Restoring the [PlaceHolderFragment]
Fragments are restored in the [updateFragment] method:
// data
private String text;
private int numVisit;
private String newText;
private boolean hasBeenVisited = false;
private ObjectMapper jsonMapper = new ObjectMapper();
...
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// Which fragment is this?
int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
int numView = numSection - 1;
// Does the fragment need to be initialized?
if (fragmentHasToBeInitialized) {
// initial text
text = getString(R.string.section_format, numSection);
fragmentHasToBeInitialized = false;
}
// navigation?
boolean navigation = session.getCurrentView() != numView;
if (navigation) {
// increment visit count
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
newText = String.format("%s, visit %s", text, numVisit);
} else {
// this is a restore
PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
newText = state.getText();
}
// display text
textViewInfo.setText(newText);
// current view
session.setCurrentView(numView);
}
- lines 15-16: determine the number of the view being updated;
- lines 18-22: case where the fragment is in a save/restore cycle after a device orientation change. It must be restored here. This generally involves restoring certain fields of the fragment;
- line 20: the [text] field on line 2 must contain the initial text displayed by the fragment: [Hello world from section i]. It must be regenerated here;
- line 21: note that the fragment has been initialized;
- lines 24–36: As with the [Vue1Fragment] fragment previously, the visit counter must not be incremented during a restore. As before, we must distinguish between navigation and restoration;
- lines 32–36: restoration case;
- line 34: the fragment’s state before the device rotation is retrieved from the session;
- line 35: the text that was displayed at that time is retrieved;
- line 38: this text is displayed again;
- line 40: the number of the new view displayed is noted in the session;
1.23.8. Tab Management
The previous sections did not address tab management. However, we encountered a problem in Example 21 when rotating the device: only the first tab [View 1] was retained. The second tab was lost.
We resolve this issue in the [MainActivity] class as follows:
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "afterViews");
}
// toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
...
// First 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);
}
// Which tab should be selected?
tabLayout.getTabAt(session.getSelectedTab()).select();
...
}
- lines 14–16: creation of the first tab;
- lines 18-23: creation of the second tab. To determine whether to create it, we check the session for the number of the fragment displayed in tab 2. If this number is not -1 (its initial value), then the second tab is created. At this point, we have two tabs, with the first one selected by default;
- line 26: we retrieve from the session the number of the tab that was selected before the save/restore and reselect it. If the [selectedTab] field has not yet been initialized by the code, its initial value of 0 is used;

















































































































































































































































































































































