Skip to content

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:

  1. Android for J2EE Developers;
  1. Introduction to Android Tablet Programming Through Examples;
  2. Controlling an Arduino with an Android Tablet;
  3. 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:

Example
Nature
1
Importing an existing Android project
2
A basic Android project
3
A basic [Android Annotations] project
4
Views and events
5
Navigation between views
6
Tab navigation
7
Using the [Android Annotations] library with Gradle
8–12
Managing fragments in an Android app
13
Revisited View Navigation
14
Two-layer architecture
15
Client/server architecture
16
Handling Asynchrony with RxJava
17, 17B
Data Entry Components
18
Using a View Pattern
19
The ListView Component
20
Using a Menu
21
Using a Parent Class for Fragments
22, 22B
Saving and restoring the state of the activity and fragments
23
Weather client
Chap 2
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.
Chap 3
Appointment management for a medical practice
Chapter 4
Practical Exercise - Basic Payroll Management
Chapter 5
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]:

 

Image

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

 

Image

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

Image

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

Image

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:

No.
Id
Type
Role
1
textViewTitleView1
TextView
View title
2
textView1
TextView
a question
3
editTextName
EditText
enter a name
4
buttonValidate
Button
to confirm the entry
5
buttonView2
Button
to switch to view #2

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:

Image

1.5.3. Event Handling

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

Image

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:

05-28 09:06:23.751 571-571/exemples.android D/MainActivity: onCreate
05-28 09:06:23.841 571-571/exemples.android D/MainActivity: afterViews

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.

Image

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

Image

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

  1. line 6: create an object of type [Intent]. This object will specify both the activity to launch and the information to pass to it;
  2. 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;
  3. 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;
  4. 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:

No.
Id
Type
Role
1
textViewTitleView2
TextView
View title
2
textViewHello
TextView
some text
5
btn_view1
Button
to go to view #1

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:

1
2
3
05-28 09:38:09.429 26711-26711/examples.android D/MainActivity: constructor
05-28 09:38:09.449 26711-26711/examples.android D/MainActivity: onCreate
05-28 09:38:09.600 26711-26711/examples.android D/MainActivity: afterViews

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:

1
2
3
05-28 09:39:26.607 26711-26711/examples.android D/SecondActivity: constructor
05-28 09:39:26.608 26711-26711/examples.android D/SecondActivity: onCreate
05-28 09:39:26.617 26711-26711/examples.android D/SecondActivity: afterViews

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:

1
2
3
05-28 09:39:56.393 26711-26711/examples.android D/MainActivity: constructor
05-28 09:39:56.394 26711-26711/examples.android D/MainActivity: onCreate
05-28 09:39:56.400 26711-26711/examples.android D/MainActivity: afterViews

The [MainActivity] class is therefore instantiated again. When you click the [View #2] button, the new logs are as follows:

1
2
3
05-28 09:40:59.099 26711-26711/examples.android D/SecondActivity: constructor
05-28 09:40:59.102 26711-26711/examples.android D/SecondActivity: onCreate
05-28 09:40:59.113 26711-26711/examples.android D/SecondActivity: afterViews

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:

Image

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:

Image

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:

Image

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:

1
2
3
4
5
6
7
8
9
05-28 10:44:32.622 29371-29371/examples.android D/MainActivity: constructor
05-28 10:44:32.626 29371-29371/examples.android D/MainActivity: onCreate
05-28 10:44:32.759 29371-29371/examples.android D/PlaceholderFragment: newInstance 1
05-28 10:44:32.759 29371-29371/examples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/examples.android D/PlaceholderFragment: newInstance 2
05-28 10:44:32.759 29371-29371/examples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/examples.android D/PlaceholderFragment: onCreateView 2
05-28 10:44:32.760 29371-29371/examples.android D/PlaceholderFragment: onCreateView 1
05-28 10:44:33.349 29371-29371/examples.android D/menu: creating menu
  • 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:

1
2
3
05-28 10:47:15.566 29371-29371/examples.android D/PlaceholderFragment: newInstance 3
05-28 10:47:15.566 29371-29371/examples.android D/PlaceholderFragment: constructor
05-28 10:47:15.566 29371-29371/examples.android D/PlaceholderFragment: onCreateView 3
  • 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:

05-28 10:48:26.630 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 1

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:

05-28 13:54:54.801 8809-8809/examples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/examples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/examples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/examples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/examples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/examples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/examples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/examples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/examples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/examples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/examples.android D/menu: creating menu
  • 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:

1
2
3
4
5
05-28 14:01:42.786 8809-8809/examples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/examples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/examples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/examples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/examples.android D/PlaceholderFragment: onResume setText 3
  • 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:

1
2
3
4
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 14:11:18.353 8809-8809/examples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/examples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/examples.android D/PlaceholderFragment: onResume setText 1

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:

1
2
3
4
05-28 14:12:59.526 8809-8809/examples.android D/PlaceholderFragment: onCreateView 3
05-28 14:12:59.527 8809-8809/examples.android D/PlaceholderFragment: afterViews 3
05-28 14:12:59.527 8809-8809/examples.android D/PlaceholderFragment: onResume 3
05-28 14:12:59.527 8809-8809/examples.android D/PlaceholderFragment: onResume setText 3

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:

05-28 13:54:54.801 8809-8809/exemples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/examples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/examples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/examples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/examples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/examples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/examples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/examples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/examples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/examples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/examples.android D/menu: creating menu

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:

1
2
3
4
5
05-28 14:01:42.786 8809-8809/examples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/examples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/examples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/examples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/examples.android D/PlaceholderFragment: onResume setText 3

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:

1
2
3
4
05-28 14:11:18.353 8809-8809/examples.android D/PlaceholderFragment: onCreateView 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/examples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/examples.android D/PlaceholderFragment: onResume setText 1

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:

1
2
3
4
05-28 14:12:59.526 8809-8809/examples.android D/PlaceholderFragment: onCreateView 3
05-28 14:12:59.527 8809-8809/examples.android D/PlaceholderFragment: afterViews 3
05-28 14:12:59.527 8809-8809/examples.android D/PlaceholderFragment: onResume 3
05-28 14:12:59.527 8809-8809/examples.android D/PlaceholderFragment: onResume setText 3

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:

06-03 02:45:42.163 2346-2346/examples.android D/MainActivity: constructor
06-03 02:45:42.331 2346-2346/examples.android D/MainActivity: afterViews
06-03 02:45:42.341 2346-2346/examples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/examples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/examples.android D/PlaceholderFragment: constructor
06-03 02:45:42.515 2346-2346/examples.android D/MainActivity: getItem[0]
06-03 02:45:42.516 2346-2346/examples.android D/MainActivity: getItem[1]
06-03 02:45:42.517 2346-2346/examples.android D/PlaceholderFragment: onCreateView 2
06-03 02:45:42.520 2346-2346/examples.android D/PlaceholderFragment: afterViews 2
06-03 02:45:42.523 2346-2346/examples.android D/PlaceholderFragment: onCreateView 1
06-03 02:45:42.524 2346-2346/examples.android D/PlaceholderFragment: afterViews 1
06-03 02:45:42.524 2346-2346/examples.android D/PlaceholderFragment: onResume 1
06-03 02:45:42.524 2346-2346/examples.android D/PlaceholderFragment: onResume setText 1
06-03 02:45:42.525 2346-2346/examples.android D/PlaceholderFragment: onResume 2
06-03 02:45:42.525 2346-2346/examples.android D/PlaceholderFragment: onResume setText 2
06-03 02:45:44.596 2346-2346/examples.android D/menu: creating menu
  • 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:

// fragment offset
    [ViewPager].setOffscreenPageLimit(n);

With the above instruction,

  1. 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];
  2. 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:

06-03 03:30:55.411 10344-10344/examples.android W/System: ClassLoader referenced unknown path: /data/app/examples.android-1/lib/x86
06-03 03:30:55.417 10344-10344/examples.android D/MainActivity: constructor
06-03 03:30:55.460 10344-10344/examples.android D/MainActivity: afterViews
06-03 03:30:55.474 10344-10344/examples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/examples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/examples.android D/PlaceholderFragment: constructor
06-03 03:30:55.559 10344-10344/examples.android D/MainActivity: getItem[0]
06-03 03:30:55.559 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/examples.android D/MainActivity: getItem[1]
06-03 03:30:55.560 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/examples.android D/MainActivity: getItem[2]
06-03 03:30:55.560 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:30:55.560 10344-10344/examples.android D/PlaceholderFragment: onCreateView 1
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: afterViews 1
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: onResume 1
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: onResume setText 1
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: onCreateView 2
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: afterViews 2
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: onResume 2
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: onResume setText 2
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: onCreateView 3
06-03 03:30:55.565 10344-10344/examples.android D/PlaceholderFragment: afterViews 3
06-03 03:30:55.565 10344-10344/examples.android D/PlaceholderFragment: onResume 3
06-03 03:30:55.565 10344-10344/examples.android D/PlaceholderFragment: onResume setText 3
06-03 03:30:56.798 10344-10344/examples.android D/menu: creating menu
  • 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:

06-03 03:34:03.388 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:34:03.388 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true

Let's switch from tab 2 to tab 3:

06-03 03:34:43.292 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:34:43.292 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true

Then from tab 3 to tab 1:

06-03 03:35:32.666 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:35:32.666 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true

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:

1
2
3
4
5
6
7
8
9
06-04 12:35:32.996 15994-15994/exemples.android D/menu: onOptionsItemSelected
06-04 12:35:32.996 15994-15994/examples.android D/menu: action_terminate selected
06-04 12:35:33.561 15994-15994/examples.android D/MainActivity: onDestroy
06-04 12:35:33.561 15994-15994/examples.android D/PlaceholderFragment: onDestroyView 1
06-04 12:35:33.562 15994-15994/examples.android D/PlaceholderFragment: onDestroy 1
06-04 12:35:33.562 15994-15994/examples.android D/PlaceholderFragment: onDestroyView 2
06-04 12:35:33.562 15994-15994/examples.android D/PlaceholderFragment: onDestroy 2
06-04 12:35:33.562 15994-15994/examples.android D/PlaceholderFragment: onDestroyView 3
06-04 12:35:33.562 15994-15994/examples.android D/PlaceholderFragment: onDestroy 3
  • 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:

05-31 06:23:07.015 32551-32551/examples.android D/MainActivity: constructor
05-31 06:23:07.041 32551-32551/examples.android D/MainActivity: afterViews
05-31 06:23:07.050 32551-32551/examples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/examples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/examples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/examples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/examples.android D/PlaceholderFragment: constructor
05-31 06:23:07.278 32551-32551/examples.android D/MainActivity: getItem[0]
05-31 06:23:07.278 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/examples.android D/MainActivity: getItem[1]
05-31 06:23:07.278 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/examples.android D/MainActivity: getItem[2]
05-31 06:23:07.278 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 3: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.280 32551-32551/examples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.291 32551-32551/examples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.294 32551-32551/examples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.295 32551-32551/examples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/examples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/examples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.798 32551-32551/examples.android D/menu: creating menu
  • 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:

1
2
3
4
5
6
05-31 06:52:36.132 32551-32551/examples.android D/MainActivity: getItem[3]
05-31 06:52:36.132 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.132 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:52:36.132 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:52:36.134 32551-32551/examples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.134 32551-32551/examples.android D/PlaceholderFragment: onResume 4: numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
  • 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:

1
2
3
4
5
6
05-31 06:58:16.228 32551-32551/examples.android D/MainActivity: getItem[4]
05-31 06:58:16.228 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 5: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.228 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:58:16.228 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:58:16.229 32551-32551/examples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.229 32551-32551/examples.android D/PlaceholderFragment: onResume 5: numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
  • 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:

05-31 07:38:27.780 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 1: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:38:27.780 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 2: numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • line 1: fragment 1 is hidden;
  • line 2: fragment 2 is displayed;

Switching from tab 2 to tab 3:

05-31 07:39:33.059 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 2: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:39:33.059 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 3: numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • line 1: fragment 2 is hidden;
  • line 2: fragment 3 is displayed;

Switching from tab 3 to tab 4:

05-31 07:40:30.362 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 3: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:40:30.362 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • line 1: fragment 3 is hidden;
  • line 2: fragment 4 is displayed;

Switching from tab 4 to tab 5:

05-31 07:41:23.479 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:41:23.479 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 5: numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • 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;

Image

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

navigateToView(...);

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:

Image

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:

No.
ID
Type
Role
1
edtNbAleas
EditText
number of random numbers to generate in the integer interval [a,b]
2
edtA
EditText
value of a
2
edtB
EditText
value of b
4
btnExecute
Button
starts number generation
5
ListView
lstAnswers
list of generated numbers in reverse order of generation. The most recently generated number is displayed first;

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:


public ArrayAdapter (Context context, int resource, int textViewResourceId, List<T> objects)
  • [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:

  1. 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);
  1. 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];
  1. 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:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.1.RELEASE)

2014-10-07 09:13:42.194  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Starting Application on Gportpers3 with PID 7408 (D:\data\istia-1415\android\dvp\exemples\exemple-10-server\target\classes started by ST in D:\data\istia-1415\android\dvp\exemples)
2014-10-07 09:13:42.294  INFO 7408 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@23edbbb9: startup date [Tue Oct 07 09:13:42 CEST 2014]; root of context hierarchy
2014-10-07 09:13:42.996  INFO 7408 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-10-07 09:13:44.251  INFO 7408 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-10-07 09:13:44.665  INFO 7408 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-10-07 09:13:44.666  INFO 7408 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.54
2014-10-07 09:13:44.866  INFO 7408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-10-07 09:13:44.866  INFO 7408 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2575 ms
2014-10-07 09:13:45.748  INFO 7408 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-10-07 09:13:45.750  INFO 7408 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-10-07 09:13:46.802  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.946  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/{a}/{b}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public android.exemples.server.web.AleaResponse android.exemples.server.web.AleaController.getAlea(int,int)
2014-10-07 09:13:46.950  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-10-07 09:13:46.951  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:47.294  INFO 7408 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-10-07 09:13:47.335  INFO 7408 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-10-07 09:13:47.337  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Started Application in 6.081 seconds (JVM running for 6.897)
  • 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):

Image

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

Image

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

  1. a [Presentation] layer (view+activity) similar to the one we studied in Example [Example-14];
  2. 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:

  1. to ensure that the [updateFragments] method of the child classes is always called when the fragment is displayed, and only once;
  2. 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;

Image

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

Image

No.
Type
Id
1
EditText
edt_nbaleas
2
TextView
txt_errorNumberOfEpisodes
3
EditText
edt_a
4
EditText
edt_b
5
TextView
txt_errorInterval
6
EditText
editTextWebServiceUrl
7
TextView
textViewErrorUrl
8
EditText
editTextDelay
9
TextView
textViewErrorDelay
10
Button
btn_Execute
11
Button
btn_Cancel
12
TextView
txt_Answers
13
ListView
lst_answers

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:

Image

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:

06-06 08:48:51.571 15317-16201/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 08:48:51.576 15317-16202/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 08:48:51.585 15317-16204/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 08:48:51.586 15317-16203/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 08:48:51.593 15317-16205/examples.android D/Dao_: getRandom [100, 200] in progress
...
06-06 08:48:53.568 15317-15317/examples.android D/View1Fragment_: Exception received
06-06 08:48:53.568 15317-15317/examples.android D/View1Fragment_: Cancellation requested
06-06 08:48:53.587 15317-15317/examples.android D/View1Fragment_: Exception received
06-06 08:48:53.587 15317-15317/examples.android D/View1Fragment_: Exception received
06-06 08:48:53.587 15317-15317/examples.android D/View1Fragment_: Exception received
06-06 08:48:53.587 15317-15317/examples.android D/View1Fragment_: Exception received
  • 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:

06-06 09:12:38.360 4640-5054/exemples.android D/Dao_: getAlea [100, 200] in progress
06-06 09:12:38.360 4640-5055/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 09:12:38.361 4640-5056/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 09:12:38.362 4640-5057/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 09:12:38.363 4640-5058/examples.android D/Dao_: getRandom [100, 200] in progress
...
06-06 09:12:39.895 4640-4640/examples.android D/View1Fragment_: Cancellation requested
06-06 09:29:56.313 1616-1616/examples.android D/View1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/examples.android D/View1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/examples.android D/View1Fragment_: showInfo(185)
06-06 09:30:00.150 1616-1616/examples.android D/View1Fragment_: showInfo(157)
06-06 09:30:00.151 1616-1616/examples.android D/View1Fragment_: showInfo(157)
  • 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:

1
2
3
4
5
6
7
06-07 05:48:09.790 28272-28272/examples.android D/Dao_: getAlea [100, 200] in progress
06-07 05:48:09.791 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:48:09.791 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:48:09.791 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:48:09.791 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:48:11.789 28272-28272/examples.android D/View1Fragment_: Exception received
06-07 05:48:11.789 28272-28272/examples.android D/View1Fragment_: Cancellation requested

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:

1
2
3
4
5
6
06-07 05:52:22.675 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:52:22.675 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:52:22.675 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:52:22.675 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:52:22.675 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:52:23.485 28272-28272/examples.android D/View1Fragment_: Cancellation requested

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:

06-07 06:03:20.778 27085-27085/examples.android D/Dao_: getAlea [100, 200] in progress
06-07 06:03:20.784 27085-27085/examples.android D/Dao_: getAlea [100, 200] in progress
06-07 06:03:20.785 27085-27085/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 06:03:20.785 27085-27085/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 06:03:20.785 27085-27085/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 06:03:21.493 27085-27085/examples.android D/View1Fragment_: Cancellation requested
06-07 06:03:21.636 27085-27440/examples.android D/Dao_: response [{"body":176,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27442/examples.android D/Dao_: response [{"body":145,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27439/examples.android D/Dao_: response [{"body":197,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27438/examples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27441/examples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
  • 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:

  1. RxAndroid can be used in an Android application that does not use AA;
  2. 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;
  3. 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:

Image

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:

  • Line 2: a vertical [ScrollView] layout. It allows you to
  • display a form larger than the tablet screen
  • tablet. You can view the entire form by
  • scrolling;
 
  • lines 125–132: a checkbox
  • lines 134–159: a group of three radio buttons
  • lines 161–166: a search bar
  • lines 16–176: a text input field
  • lines 178–186: a yes/no switch
  • lines 188–195: a time input field
  • lines 197–207: a multi-line text box
  • lines 209–215: a drop-down list
  • lines 217-225: a date input field
  • All other components are [TextView]s that display text.
 

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:

Image

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:

  

Image

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

Image

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:

Image

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

Image

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:

07-13 04:08:27.188 1677-1677/examples.android D/MainActivity: constructor
07-13 04:08:27.189 1677-1677/examples.android D/MainActivity: afterInject
07-13 04:08:27.190 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/examples.android D/AbstractFragment: constructor View1Fragment_
07-13 04:08:27.190 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.194 1677-1677/examples.android D/MainActivity: afterViews
07-13 04:08:27.195 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/examples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.203 1677-1677/examples.android D/PlaceholderFragment: afterViews 4 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.204 1677-1677/examples.android D/PlaceholderFragment: afterViews 3 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.208 1677-1677/examples.android D/View1Fragment: afterViews View1Fragment_ - numVisit=0
07-13 04:08:27.208 1677-1677/examples.android D/PlaceholderFragment: afterViews 2 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.209 1677-1677/examples.android D/PlaceholderFragment: afterViews 1 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.351 1677-1677/examples.android D/menu: creating menu
07-13 04:08:27.351 1677-1677/examples.android D/PlaceholderFragment_: creating menu
07-13 04:08:27.351 1677-1677/examples.android D/PlaceholderFragment: update 3 - PlaceholderFragment_ - numVisit=0, initDone=true, getActivity()==null:false
  • 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;