1. 学习 Android 编程
该文档的PDF版本可在此处获取 |HERE|。
文档中的示例可在此处查看 |HERE|。
1.1. 简介
1.1.1. 目录
本文档是对以下若干现有文档的改写:
并引入了以下新功能:
- 文档 1 提出了一种名为 AVAT(Activity-Views-Actions-Tasks)的架构,以简化 Android 应用程序中的异步编程。在该文档中,使用标准的 RxJava 库来管理异步操作;
- 文档 2 使用了带 Android 插件的 Eclipse IDE。本文档使用 Android Studio;
- 文档 3 保持原样;
- 文档 4 曾结合 IntelliJ IDEA Community Edition IDE 使用 [Android Annotations] (AA) 库。本文完整复现了文档 4 的内容,但存在以下差异:
- IDE 现为 Android Studio;
- 所有客户端或服务器项目的构建系统均采用 Gradle(文档 4 中有时使用 Maven)
- 异步编程采用 RxJava 库实现(文档 4 中使用的是 AA 库);
- 本文探讨了前文未涉及或仅简要提及的领域:
- 片段邻接性概念;
- 保存/恢复 Activity 及其片段;
- 片段的生命周期;
最后,本文呈现了一个与 Web 服务/JSON 通信的 Android 客户端框架,其中我们提取了此类客户端中常见的大量元素。从第 2 章开始的所有示例均采用此框架。这是本文真正具有创新性的部分。
以下示例将进行介绍:
自然 | |
导入现有的 Android 项目 | |
一个基本的 Android 项目 | |
一个基本的 [Android Annotations] 项目 | |
视图与事件 | |
视图间的切换 | |
标签页导航 | |
在 Gradle 中使用 [Android Annotations] 库 | |
在 Android 应用中管理片段 | |
重新审视视图导航 | |
双层架构 | |
客户端/服务器架构 | |
使用 RxJava 处理异步操作 | |
数据输入组件 | |
使用视图模式 | |
ListView 组件 | |
使用菜单 | |
为片段使用父类 | |
保存和恢复 Activity 及片段的状态 | |
天气客户端 | |
一个与Web服务/JSON通信的Android客户端框架。它涵盖了此类Android客户端中常见的大量元素。 | |
医疗机构预约管理 | |
实践练习——基础薪资管理 | |
实践练习——订购Arduino开发板 |
本文档曾用于昂热大学[istia.univ-angers.fr] IstiA工程学院的大四课程。这解释了文本中偶尔出现的略显独特的语气。这两项实践练习是实验室作业,仅提供了解决方案的大致框架。解决方案必须由读者自行推导。
示例的源代码可在此处获取 |HERE|。要运行这些示例,必须按照第6.12节中的步骤操作。
本文档是 Android 编程的入门指南。它并非旨在面面俱到,主要面向初学者。
Android 编程的参考网站位于 [http://developer.android.com/guide/components/index.html]。如需了解 Android 编程的概览,请访问该网站。
1.1.2. 先决条件
为充分利用本文档,您应具备扎实的 Java 编程语言基础。
1.1.3. 使用的工具
以下示例已在以下环境中经过测试:
- Windows 10 Pro 64 位系统;
- JDK 1.8;
- Android SDK API 23;
- Android Studio,版本 2.1;
- Genymotion 模拟器,版本 2.6.0;
要按照本文操作,您必须安装:
- JDK(参见第 6.8 节);
- Genymotion Android 模拟器管理器(参见第 6.9 节);
- Maven 依赖管理器(参见第 6.10 节);
- [Android Studio] 集成开发环境(IDE)(参见第 6.11 节);
1.2. 示例-01:导入一个 Android 示例
1.2.1. 创建项目
让我们使用 Android Studio 创建第一个 Android 项目。首先,创建一个空的 [examples] 文件夹,所有项目都将存储在此处:
![]() |
然后使用 Android Studio 创建一个项目。我们将首先导入 IDE 中附带的示例之一 [1-5]:
![]() |

![]() | ![]() |
导入项目时可能会因项目创建时使用的环境与当前运行环境不匹配而导致错误。这为我们提供了一个了解如何解决此类错误的机会。这里出现了以下错误:
![]() | ![]() |
导入的项目由以下 [build.gradle] 文件配置 [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 that is 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"
}
}
- 报告的错误是由于第 31 行、第 34–35 行引起的:我们没有 SDK 21。我们将此版本替换为我们拥有的 23 版。
在 [build.gradle] 文件中,Android Studio 会给出如下建议:
![]() |
要接受这些建议,请在建议上按下 [Alt-Enter]:
![]() |
您还可能会遇到与 Gradle 版本相关的错误:
![]() |
此错误源于项目 [build.gradle] 文件要求的 Gradle 版本(如下文第 6 行所示的 2.10 版)与当前版本不匹配:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
以及 [<project>/gradle/wrapper/gradle-wrapper.properties] 文件中列出的那个:
#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
在上述第 6 行中,将 2.8 替换为 2.10。
要访问文件 [<project>/gradle/wrapper/gradle-wrapper.properties],请使用项目视图:
![]() | ![]() | ![]() |
修正此问题后,即可编译应用程序 [1]、启动 Genymotion 模拟器 [2] 并运行项目 [3]:
![]() | ![]() | ![]() |
![]() |

让我们停止该应用程序:
![]() |
现在您可以关闭该项目了。我们将创建一个新的项目。
![]() |
1.2.2. 关于 IDE 的几点说明
1.2.2.1. 视图
Android Studio (AS) IDE 为项目开发提供了多种视图。我们将主要使用以下两种:
- [Android] 视图 [1]:
- [项目]视图 [4];
![]() | ![]() |
![]() |
大多数情况下,我们会使用 [Android] 视图。当我们将一个项目克隆到另一个项目中时,则需要使用 [Project] 视图。
1.2.2.2. 运行管理
有几种方法可以运行、停止或重新运行 AS 项目。首先,工具栏上有相应的按钮:
![]() | ![]() | ![]() |
[重新运行] 按钮 [3] 会先停止项目 [2],然后重新启动它 [1]。
1.2.2.3. 缓存管理
Android Studio 会维护其所管理项目的缓存,以尽可能提高 IDE 的响应速度。在 Android Studio 2.1 版本(2016 年 5 月)中,该缓存往往无法及时反映刚刚进行的代码更改。在这种情况下,您必须清除缓存:
![]() | ![]() |
在 Android 2.1(2016 年 5 月)版本中,上述步骤需要重复执行多次,有时甚至仍无法解决检测到的问题。解决方法是禁用 [即时运行]:
![]() | ![]() |
- 在[3-4]中,所有选项均已禁用;
在以下所有操作中,我们均采用此缓存配置,且未遇到任何问题。
1.2.2.4. 日志管理
运行项目时,日志会在 Android Monitor 中显示:
![]() | ![]() |
在 [Android Monitor] 选项卡 [1] 中,日志会显示在 [logcat] 选项卡 [2] 中。点击 [3] 按钮可清除日志。当您需要查看特定操作的日志时,此按钮非常有用:
- 清除日志;
- 在 Android 设备上执行您需要查看日志的操作;
- 此时显示的日志即与该操作相关的日志;
日志级别有多种 [4]。默认情况下,系统会选择 [详细] 模式。这意味着将显示所有级别的日志。您可以使用 [4] 选择特定的日志级别。
日志对于确定项目执行过程中某些方法在何时被调用非常有用。我们将频繁使用它们。让我们看看 [Example-01] 项目中 [MainActivity] 类的代码:
![]() |
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);
}
}
在上文中,方法 [onCreate(第 14 行)] 和 [onCreateOptionsMenu(第 26 行)] 是父类 [Activity](第 9 行)的方法。它们在应用程序生命周期的不同阶段被调用。 有时这些方法会被多次执行。即使查阅文档,也很难判断某个生命周期方法是在我们自己编写的方法之前还是之后执行。然而,了解这一信息往往非常重要。因此,我们可以像下面所示那样添加日志:
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()) {
...
}
}
- 第 7、14 和 21 行使用了 [Log] 类。该类允许您将日志写入 Android 控制台 [logcat]。日志分为多个级别(info、warning、debug、verbose、error)。 [Log.d] 用于显示 [debug] 级别的日志。其第一个参数是日志消息的来源。实际上,不同的来源可以向日志控制台发送消息。为了区分它们,我们使用这个第一个参数。第二个参数是要写入日志控制台的消息;
如果我们再次运行 [Example-01] 项目,将获得以下日志:
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
我们可以看到,创建 Android 活动的 [onCreate] 方法在创建应用菜单的 [onCreateOptionsMenu] 方法之前执行。
现在,如果我们在 Android 模拟器中点击菜单选项 [1]:
![]() |
以下日志已添加到日志控制台:
05-28 08:41:22.881 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onOptionsItemSelected
接下来,我们经常会在 Android 代码中添加日志语句。大多数情况下,我们不会对它们进行注释。它们的存在只是为了引导读者查看日志控制台,从而逐步理解 Android 应用程序的生命周期。
1.2.2.5. 管理模拟器 [Genymotion]
有时,Genymotion 模拟器会崩溃且无法重启。这是因为任务管理器中仍有 VirtualBox 进程在运行。请打开任务管理器 [Ctrl-Alt-Del] 并删除所有 VirtualBox 进程:
![]() | ![]() |
完成此操作后,从 Android Studio 重新启动 Genymotion 模拟器。
1.2.2.6. 管理生成的 APK 二进制文件
编译项目后会生成一个扩展名为 .apk 的二进制文件:
![]() | ![]() | ![]() |
共有两个版本:一个名为 [debug],另一个名为 [debug-unaligned]。您应使用前者;后者是一个中间版本。在 [4] 中生成的 .apk 二进制文件可直接传输到模拟器或 Android 设备。若要将其传输到模拟器,只需用鼠标将其拖放到模拟器上即可。
1.3. 示例-02:一个基本的 Android 项目
让我们使用 Android Studio [1-12] 创建一个新的 Android 项目:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() | ![]() | ![]() |
在[13]中,我们运行了该应用程序。随后,我们在Genymotion模拟器上看到了[14]中所示的界面。
1.3.1. Gradle 配置
生成的项目通过以下 [build.gradle] 文件进行配置:
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.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'
}
此文件由 IDE 根据其配置设置自动生成。这是一个基础文件,我们将逐步对其进行扩展。
- 第 3–12 行:Android 应用程序的特性;
- 第 22–25 行:其依赖项。我们将主要根据所学的示例在此处进行修改;
1.3.2. 应用程序清单
![]() |
[AndroidManifest.xml] [1] 文件定义了 Android 应用程序二进制文件的特性。其内容如下:
<?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>
- 第 3 行:Android 项目包名;
- 第 10 行:Activity 名称;
这两项信息来自项目创建时填写的条目:
![]() |
- 清单(包)的第3行来自上文的条目[4]。该包中自动生成了若干类;
![]() |
- 清单的第 10 行(活动名称)来自上文的条目 [1];
让我们回到清单文件:
<?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>
![]() | ![]() | ![]() |
- 第 10 行:应用程序的主活动。它引用了上文中的类 [1];
- 第 6 行:应用程序图标 [2]。该图标可以更改;
- 第 7 行:应用程序的标签。该标签位于 [strings.xml] 文件中 [3]:
<resources>
<string name="app_name">Exemple-02</string>
</resources>
[strings.xml] 文件包含应用程序使用的字符串。第 2 行:应用程序名称来自构建项目时所做的设置 [4]:
![]() |
- 第 10 行:一个 Activity 标签。一个 Android 应用程序可以包含多个 Activity;
- 第 12 行:该 Activity 被指定为主 Activity;
- 第 13 行:且它必须出现在 Android 设备上可启动的应用程序列表中。
1.3.3. 主活动
![]() | ![]() |
一个 Android 应用基于一个或多个 Activity。这里已生成一个 Activity [1]:[MainActivity]。根据其类型,一个 Activity 可以显示一个或多个视图。生成的 [MainActivity] 类如下:
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);
}
}
- 第 6 行:[MyActivity] 类继承了 Android 的 [AppCompatActivity] 类。未来所有 Activity 都将采用这种方式;
- 第 9 行:当 Activity 被创建时,[onCreate] 方法会被执行。这发生在与该 Activity 关联的视图显示之前;
- 第 10 行:调用父类的 [onCreate] 方法。这必须始终执行;
- 第 11 行:[activity_main.xml] 文件 [2] 是与该 Activity 关联的视图。该视图的 XML 定义如下:
<?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="exemples.android.MainActivity">
<TextView
android:text="Hello World!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- b-k 行:布局管理器。默认选择的是 [RelativeLayout] 类型。在此类容器中,组件的位置是相对于彼此的(位于右侧、左侧、下方、上方);
- 第 m-p 行:用于显示文本的 [TextView] 组件;
- 第 n 行:显示的文本。不建议直接在视图中硬编码文本。最好将此文本移至 [res/values/strings.xml] 文件 [3] 中:
因此,显示的文本将是 [Hello World!]。它将显示在何处?[RelativeLayout] 容器将填满整个屏幕。作为其唯一元素的 [TextView] 将显示在该容器的顶部和左侧,因此也位于屏幕的顶部和左侧;
第 11 行中的 [R.layout.activity_main] 是什么意思?每个 Android 资源(视图、片段、组件等)都会被分配一个标识符。因此,位于 [res/layout] 文件夹中的 [V.xml] 视图将被标识为 [R.layout.V]。R 是一个生成在 [app/build/generated] 文件夹中的类 [1-3]:
![]() |
[R] 类的定义如下:
...............
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;
}
- 第 14 行:属性 [R.layout.activity_main] 是视图 [res/layout/activity_main.xml] 的标识符;
- 第 7 行:[R.string.app_name] 属性对应于文件 [res/values/string.xml] 中的字符串 ID [app_name]:
- 第 19 行:属性 [R.mipmap.ic_launcher] 是图像 [res/mipmap/ic_launcher] 的标识符;
因此,请记住,当你在代码中引用 [R.layout.activity_main] 时,实际上是在引用 [R] 类的某个属性。IDE 会帮助你识别该类的不同元素:
![]() | ![]() |
1.3.4. 运行应用程序
要运行一个 Android 应用程序,我们需要创建一个运行配置:
![]() | ![]() | ![]() |
- 在 [1] 中,选择 [编辑配置];
- 该项目创建时包含一个 [app] 配置,我们将删除 [2] 并重新创建;
- 在 [3] 中,创建一个新的运行配置;
![]() |
- 在 [4] 中,选择 [Android 应用程序];

- 在 [5] 中,从下拉列表中选择 [app] 模块;
- 在 [6-8] 中,保留默认值;
- 在 [7] 中,默认 Activity 是 [AndroidManifest.xml] 文件中定义的(如下所示的第 1 行):
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
- 在 [8] 中,选择 [显示选择器对话框] 以选择应用将运行的设备(模拟器、平板电脑);
- 在 [9] 中,指定应保存此选择;
- 确认配置;
![]() |
- 在 [11] 中,启动模拟器管理器 [Genymotion](参见第 6.9 节);
![]() |
- 在 [12] 中,选择一个平板电脑模拟器并启动它 [13];
![]() | ![]() |
- 在 [14] 中,运行 [app] 执行配置;
- 在 [15] 中,将显示运行时设备选择表单。此处仅有一个选项:之前启动的 [Genymotion] 模拟器;
片刻之后,软件模拟器将显示以下界面:

1.3.5. Activity的生命周期
让我们回到 [MainActivity] 活动的代码:
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);
}
}
第 8 至 12 行中的 [onCreate] 方法是可以在 Activity 生命周期中调用的方法之一。Android 文档中列出了这些方法:
![]() |
- [1]:当 Activity 启动时,会调用 [onCreate] 方法。正是在此方法中,Activity 与视图建立关联,并获取其组件的引用;
- [2-3]:随后会调用 [onStart] 和 [onResume] 方法。请注意,[onResume] 方法是当前运行的活动进入状态 [4] 之前执行的最后一个方法;
1.4. 示例-03:使用 [Android Annotations] 库重写 [示例-02] 项目
接下来我们将介绍 [Android Annotations] 库,它能简化 Android 应用程序的编写。为此,请按照步骤 [1-16] 将 [Example-02] 示例复制为 [Example-03]。
![]() | ![]() |
- 在 [1] 中,选择 [Project] 视图以查看整个 Android 项目;
![]() | ![]() |
![]() | ![]() |
![]() | ![]() | ![]() | ![]() |
注:在[14]和[15]之间,我们从[Android]视图切换到了[Project]视图(参见第1.2.2.1节)。
然后,我们修改文件 [res/values/strings.xml] [17]:
![]() |
[strings.xml] 文件的修改如下:
<resources>
<string name="app_name">Exemple-03</string>
</resources>
现在,我们运行这个新应用程序,它保留了 [示例-02] 中的所有配置:
![]() | ![]() |
在[19]中,我们得到了与[Example-02]相同的结果,但名称有所不同。
接下来我们将介绍 [Android Annotations] 库,以下简称 AA。该库引入了用于标注 Android 源代码的新类。这些注解将被一个处理器所使用,该处理器将在模块中生成新的 Java 类;这些类将像开发者编写的类一样参与模块的编译。因此,我们拥有以下构建链:
![]() |
首先,我们将 AA 注解编译器(即上述处理器)的依赖项添加到 [build.gradle] 文件中:
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'])
}
- 第 4–5 行添加了构成 AA 库的两个依赖项;
再次修改 [build.gradle] 文件以使用名为 [android-apt] 的插件,该插件将编译过程分为两个步骤:
- 处理 Android 注解,生成新类;
- 编译项目中的所有类;
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
- 第 8 行:将在中央 Maven 仓库中搜索的 [android-apt] 插件版本(第 3 行);
- 第 13 行:激活此插件;
此时,请验证 [app] 运行配置是否仍能正常工作。
现在,我们将 AA 库中的第一个注解引入 [MainActivity] 类:
![]() |
[MainActivity] 类目前如下所示:
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);
}
}
我们在第 1.3.3 节中已经解释过这段代码。我们将它修改如下:
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);
}
}
- 第 7 行:[@EActivity] 注解是一个 AA 注解(第 3 行)。其参数是与该 Activity 关联的视图;
该注解将生成一个从 [MainActivity] 类派生的 [MainActivity_] 类,而该类即为实际的 Activity。因此,我们必须按如下方式修改项目清单文件 [AndroidManifest.xml]:
<?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>
- 第 11 行:新的 Activity;
完成上述操作后,我们可以编译该项目 [1]:
![]() | ![]() |
- 在 [2] 中,我们可以看到 [MainActivity_] 类已生成在 [app/build/generated/source/apt/debug] 文件夹中;
生成的 [MainActivity_] 类如下:
//
// 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);
}
...
- 第 24-25 行:[MainActivity_] 类继承自 [MainActivity] 类;
我们不会试图解释由 AA 生成的类代码。它们处理了注解旨在隐藏的复杂性。但当你想了解所用注解是如何被“翻译”时,查看这些代码有时会有所帮助。
现在我们可以再次运行 [app] 配置。结果与之前相同。接下来我们将以此项目为起点,通过复制它来介绍 Android 编程的核心概念。
1.5. 示例-04:视图与事件
1.5.1. 创建项目
我们将遵循第 1.4 节中 [示例-03] 里描述的复制 [示例-02] 的步骤:
我们将:
- 将 [示例-03] 项目复制为 [示例-04](在 [示例-03] 中删除 [app/build] 文件夹后);
- 加载 [Example-04] 项目;
- 在文件 [app/res/values/strings.xml] 中修改项目名称(Android 视图);
- 删除文件 [Example-04/Example-04.iml](项目视图);
- 编译并运行该项目;
![]() | ![]() |
1.5.2. 构建视图
现在,我们将使用图形化编辑器来修改 [Example-04] 项目所显示的视图:
![]() | ![]() |
- 在 [1-4] 中,创建一个新的 XML 视图;
- 在 [5] 中,为视图命名;
- 在 [6] 中,指定视图的根标签。这里,我们选择一个 [RelativeLayout] 容器。在此组件容器内,组件之间采用相对定位:位于“右侧”、“左侧”、“下方”或“上方”;
![]() |
生成的 [vue1.xml] 文件 [7] 如下:
<?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>
- 第 2 行:一个空的 [RelativeLayout] 容器,它将占据平板电脑的整个宽度(第 3 行)和整个高度(第 4 行);
![]() | ![]() |
- 在 [1] 中,在显示的 [vue1.xml] 视图中选择 [设计] 选项卡;
- 在 [2-4] 中,切换至平板模式;
![]() | ![]() | ![]() |
- 在 [5] 中,将平板电脑的缩放比例设置为 1;
- 在[6]中,为平板电脑选择“横向”模式;
- 截图[7]总结了所做的设置。
![]() | ![]() | ![]() |
- 在 [1] 中,选择一个 [大文本] 并将其拖放到 [2] 视图上;
- 在 [3] 中,双击该组件;
- 在 [4] 中,编辑显示的文本。我们不会将其硬编码在 XML 视图中,而是将其外部化到文件 [res/values/string.xml] 中
![]() | ![]() | ![]() |
- 在 [5] 中,向 [strings.xml] 文件添加一个新值;
- 在 [8] 中,为该字符串分配一个标识符;
- 在 [9] 中,为该字符串赋值;
- 在 [10] 中,验证上一步操作后显示的新视图;
![]() | ![]() | ![]() |
- 双击组件后,我们更改其 ID [11];
- 在 [12] 中,在组件属性中将字体大小更改为 [50pt];
- 在 [13] 中,显示新的视图;
文件 [vue1.xml] 已按以下方式更改:
<?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/titre_vue1"
android:id="@+id/textViewTitreVue1"
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>
- GUI 中的更改位于第 10、11 和 14 行。该 [TextView] 的其他属性要么是默认值,要么是由组件在视图中的定位决定的;
- 第 7–8 行:组件的大小(包括高度和宽度)与所包含文本的大小一致(wrap_content);
- 第 13 行:组件顶部与视图顶部对齐(第 13 行),并向下偏移 50 像素(第 13 行);
- 第 12 行:组件的左侧与视图的左侧对齐(第 13 行),向右偏移 213 像素(第 12 行);
通常,左、右、上、下边距的确切尺寸将直接在 XML 中设置。
按照相同的方法,创建以下视图 [1]:
![]() |
组件如下:
将组件相互定位可能会让人感到沮丧,因为图形编辑器的行为有时难以预测。使用组件的属性可能更合适:
[textView1] 组件必须放置在标题下方 50 像素处,并距离容器的左边缘 50 像素:
![]() | ![]() | ![]() |
- 在 [1] 中,该组件的顶边与 [textViewTitreVue1] 组件的底边对齐,间距为 50 像素 [3](顶部);
- 在 [2] 中,该组件的左边缘(left)与容器的左边缘对齐,间距为 50 像素 [3](left);
[editTextNom] 组件必须放置在 [textView1] 组件右侧 60 像素处,并与其底部对齐;
![]() | ![]() |
- 在 [1] 中,该组件的左边缘与 [textView1] 组件的右边缘对齐,间距为 60 像素 [2](左侧)。它与 [textView1] 组件的底部边缘(bottom:bottom)对齐 [1];
[buttonValider] 组件必须放置在 [editTextNom] 组件右侧 60 像素处,并将其底部与该组件对齐;
![]() | ![]() |
- 在 [1] 中,该组件的左边缘与 [editTextNom] 组件的右边缘对齐,间距为 60 像素 [2](左侧)。它与 [editTextNom] 组件的底部边缘对齐(bottom:bottom)[1];
[buttonVue2]组件必须位于[textView1]组件下方50像素处,并对其左侧对齐;
![]() | ![]() |
- 在 [1] 中,该组件的左边缘与 [textView1] 组件的左边缘对齐,并位于其下方(top:bottom),距离为 50 像素 [2](top);
生成的 XML 文件如下:
<?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/titre_vue1"
android:id="@+id/textViewTitreVue1"
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_nom"
android:id="@+id/textView1"
android:layout_below="@+id/textViewTitreVue1"
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/editTextNom"
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_valider"
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>
此处包含所有图形元素。创建视图的另一种方法是直接编辑此文件。一旦习惯了这种方式,它会比使用图形编辑器更快捷。
- 第 38 行包含了一些我们尚未展示的信息。这些信息是通过 [editTextNom] 组件的属性提供的 [1]:
![]() | ![]() |
所有文本均来自以下 [strings.xml] [2] 文件:
<resources>
<string name="app_name">Exemple-04</string>
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
</resources>
现在,让我们修改 [MainActivity],使其在应用启动时显示此视图:
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);
}
}
- 第 7 行:该 Activity 现已显示 [vue1.xml] 视图;
按以下方式修改 [AndroidManifest.xml] 文件:
<?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_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- 第 12 行:此配置行可防止 [vue1] 视图显示时立即弹出键盘。这是因为该视图包含一个输入字段,且在视图显示时该字段处于焦点状态。默认情况下,这种焦点状态会导致虚拟键盘弹出;
运行应用程序并验证 [view1.xml] 视图是否确实显示:

1.5.3. 事件处理
现在,让我们处理 [View1] 视图中 [Validate] 按钮的点击事件:

[MainActivity] 的代码更改如下:
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 {
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews(){
Log.d("MainActivity","afterViews");
}
// event manager
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
Toast.makeText(this, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- 第 17–18 行: 我们将字段 [protected EditText editTextNom] 与视觉界面中由 [R.id.editTextNom] 标识的组件相关联。与该组件相关的字段必须在派生类 [MainActivity_] 中可访问,因此不能具有 [private] 作用域。由 [R.id.editTextNom] 标识的字段来自视图 [vue1.xml]:
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextNom"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
注意:请勿在 [id] 标识符中使用带重音的字符。AA 无法正确处理此类字符。
- 第 32 行:注解 [@Click(R.id.buttonValider)] 指定了处理 ID 为 [R.id.buttonValider] 的按钮上“Click”事件的方法。该 ID 同样来自视图 [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"/>
- 第 35 行:显示输入的名称:
- Toast.makeText(...).show(): 在屏幕上显示文本,
- makeText 的第一个参数是 Activity,
- 第二个参数是要在由 makeText 显示的对话框中显示的文本,
- 第三个参数是对话框的显示时长:Toast.LENGTH_LONG 或 Toast.LENGTH_SHORT;
- 第 26 行,[@AfterViews] 注解标记了该方法将在所有带有 [@ViewById] 注解的字段初始化完成后执行。了解这些字段何时初始化非常重要。例如,我们能否在 [onCreate] 方法中使用第 18 行的引用?为解答此问题,我们添加了日志;
运行 [Example-04] 项目,并验证点击 [Validate] 按钮时是否会发生某些变化。我们得到以下日志:
我们得出结论:当 [onCreate] 方法运行时,标注了 [@ViewById] 的字段尚未初始化。再次提醒初学者,建议在管理应用程序生命周期的方法中加入此类日志。
1.6. 示例-05:在视图之间导航
在上一项目中,[View 2]按钮并未被使用。我们建议通过创建第二个视图并演示如何在视图之间导航来加以利用。解决此问题有多种方法。此处提出的方法是将每个视图与一个Activity关联。另一种方法是使用单个[AppCompatActivity]来显示[Fragment]视图。这将是未来应用程序中采用的方法。
1.6.1. 创建项目
我们将 [Example-04] 项目复制为 [Example-05]。为此,我们将遵循第 1.4 节中描述的将 [Example-02] 复制为 [Example-03] 的步骤,该步骤已在第 1.5 节中重现。
![]() | ![]() |
1.6.2. 添加第二个活动
为了管理第二个视图,我们将创建第二个活动。该活动将管理视图 #2。此处我们采用“一个活动对应一个视图”的模型。当然,也可以采用其他模型。

- 在 [1-4] 中,我们创建了一个新的 Activity;

- 在 [5] 中,指定将生成的类名;
- 在 [6] 中,输入与新 Activity 关联的视图名称(view2.xml);
![]() |
- 在 [7-8] 中,受先前配置影响的文件;
[SecondActivity] 的实现如下:
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);
}
}
- 第 11 行:该 Activity 关联了视图 [vue2.xml];
视图 [vue2.xml] 如下所示:
<?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="exemples.android.SecondActivity">
</RelativeLayout>
这目前是一个带有 [RelativeLayout] 布局管理器的空视图(第 2 行)。在第 11 行,我们可以看到它已被关联到新的 Activity。
Android 模块清单 [AndroidManifest.xml] 已按以下方式更改:
<?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_"
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>
第 20 行:已注册第二个活动。
1.6.3. 从视图 1 导航到视图 2
让我们回到显示视图 1 的 [MainActivity] 类的代码。目前,向视图 2 的过渡尚未处理:
![]() |
我们的处理方式如下:
// navigate to view no. 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
// navigate to view no. 2 by passing it the name entered in view no. 1
// create an Intent
Intent intent = new Intent();
// we associate this Intent with an activity
intent.setClass(this, SecondActivity.class);
// we associate information with this Intent
intent.putExtra("NOM", editTextNom.getText().toString().trim());
// launch the [SecondActivity] activity by passing it the Intent
startActivity(intent);
}
- 第 2-3 行:[navigateToView2] 方法处理对 [vue1.xml] 视图中定义的、标识为 [R.id.buttonVue2] 的按钮的“点击”操作:
<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"/>
注释描述了视图更改的操作步骤:
- 第 6 行:创建一个 [Intent] 类型的对象。该对象将指定要启动的活动以及要传递给它的信息;
- 第 8 行:将 Intent 与一个 Activity 关联,本例中是类型为 [SecondActivity] 的 Activity,它将负责显示视图 #2。请记住,[MainActivity] 显示视图 #1。因此,我们遵循“一个视图 = 一个 Activity”的原则。我们需要定义 [SecondActivity] 类型;
- 第 10 行:可选地,向 [Intent] 对象添加信息。这些信息是为即将启动的 [SecondActivity] 准备的。[Intent.putExtra] 的参数格式为 (Object key, Object value)。请注意,[EditText.getText()] 方法返回的是文本字段中输入的文本,其返回类型并非 [String],而是 [Editable]。 必须使用 [toString] 方法才能获取输入的文本;
- 第 12 行:启动由 [Intent] 对象定义的活动。
运行 [Example-05] 项目,并确认看到视图 #2(目前为空):
![]() | ![]() |
1.6.4. 构建视图 #2
![]() | ![]() |
- 在 [1-2] 中,我们删除了不再需要的 [main.xml] 视图,然后将 [vue2.xml] 视图修改如下:
![]() |
组件如下所示:
XML 文件 [vue2.xml] 内容如下:
<?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="exemples.android.SecondActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue2"
android:id="@+id/textViewTitreVue2"
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/textViewBonjour"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_below="@+id/textViewTitreVue2"
android:layout_marginTop="50dp"
android:layout_marginLeft="50dp"
android:textSize="30sp"
android:text="Bonjour !"
android:textColor="#ffffb91b"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue1"
android:id="@+id/buttonVue1"
android:layout_marginTop="50dp"
android:textSize="30sp"
android:layout_alignLeft="@+id/textViewBonjour"
android:layout_below="@+id/textViewBonjour"/>
</RelativeLayout>
运行 [Example-05] 项目,并确认点击 [View #2] 按钮时能看到新视图。
1.6.5. [SecondActivity] 活动
在 [MainActivity] 中,我们编写了以下代码:
// navigate to view no. 2
protected void navigateToView2() {
// navigate to view no. 2 by passing it the name entered in view no. 1
// create an Intent
Intent intent = new Intent();
// we associate this Intent with an activity
intent.setClass(this, SecondActivity.class);
// we associate information with this Intent
intent.putExtra("NOM", edtNom.getText().toString().trim());
// launch the [SecondActivity] activity by passing it the Intent
startActivity(intent);
}
在第 9 行,我们向 [SecondActivity] 传递了未被使用的信息。现在我们正在使用它,这发生在 [SecondActivity] 的代码中:
![]() |
[SecondActivity] 的代码如下所示:
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 {
// visual interface components
@ViewById
protected TextView textViewBonjour;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews() {
// recover intent if it exists
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// we retrieve the name
String nom = extras.getString("NOM");
if (nom != null) {
// we display it
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
}
}
}
- 第 11 行:我们使用 [@EActivity] 注解来表明 [SecondActivity] 类是与 [vue2.xml] 视图关联的活动;
- 第 15–16 行:我们获取了由 [R.id.textViewBonjour] 标识的 [TextView] 组件的引用。这里我们没有编写 [@ViewById(R.id.textViewBonjour)]。在这种情况下,AA 会假设组件的标识符与注解字段(此处为 [textViewBonjour] 字段)完全一致;
- 第 23 行:[@AfterViews] 注解标记了一个必须在带有 [@ViewById] 注解的字段初始化完成后执行的方法。在 [OnCreate] 方法(第 19 行)中,这些字段尚无法使用,因为它们尚未被初始化。 在 [Example-05] 项目中,我们会在不同 Activity 之间切换,起初并不清楚标注了 [@AfterViews] 的方法是在 Activity 的初始实例化时执行一次,还是在每次启动 Activity 时都执行。测试表明,第二种假设是正确的;
- 第 26 行:[AppCompatActivity] 类有一个 [getIntent] 方法,该方法返回与该 Activity 关联的 [Intent] 对象;
- 第 28 行:[Intent.getExtras] 方法返回一个 [Bundle] 对象,这是一种包含与该 Activity 的 [Intent] 对象相关信息的字典;
- 第 31 行:我们获取存储在该 Activity 的 [Intent] 对象中的名称;
- 第 34 行:我们将其显示出来。
提醒:带有 [@ViewById] 注解的字段中不得包含带重音的字符。
让我们回到 [SecondActivity] 类。因为我们写了:
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
AA 将生成一个从 [SecondActivity] 派生的 [SecondActivity_] 类,该类将成为实际的 Activity。这导致我们需要在以下位置进行修改:
[MainActivity]
// navigate to view no. 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
..
// we associate this Intent with an activity
intent.setClass(this, SecondActivity_.class);
...
}
- 在第 6 行,我们必须将 [SecondActivity] 替换为 [SecondActivity_];
[AndroidManifest.xml]
<?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_"
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>
- 在第 20 行,将 [SecondActivity] 替换为 [SecondActivity_];
测试此新版本。在视图 #1 中输入一个名称,并验证视图 #2 是否正确显示该名称。
![]() | ![]() |
1.6.6. 从视图 #2 导航至视图 #1
要从视图 #2 导航到视图 #1,我们将遵循之前介绍的步骤:
- 在显示视图 2 的 [SecondActivity] 活动中放置导航代码;
- 在显示视图 1 的 [MainActivity] 中编写 [@AfterViews] 方法;
[SecondActivity] 的代码如下所示:
@Click(R.id.buttonVue1)
protected void navigateToView1() {
// we create an Intent for activity [MainActivity]
Intent intent1 = new Intent();
intent1.setClass(this, MainActivity_.class);
// retrieve the Intent of the current activity [SecondActivity]
Intent intent2 = getIntent();
if (intent2 != null) {
Bundle extras2 = intent2.getExtras();
if (extras2 != null) {
// we put the name in the Intent of [MainActivity]
intent1.putExtra("NOM", extras2.getString("NOM"));
}
// launch [MainActivity]
startActivity(intent1);
}
}
- 第 1-2 行:将 [navigateToView1] 方法与 [btn_vue1] 按钮的点击事件关联;
- 第 4 行:创建一个新的 [Intent];
- 第 5 行:关联 [MainActivity_] 活动;
- 第 7 行:获取与 [SecondActivity] 关联的 Intent;
- 第 9 行:从该 Intent 中获取信息;
- 第 12 行:从 [intent2] 中获取 [NAME] 键,并将其与相同关联值一起放入 [intent1] 中;
- 第 15 行:启动 [MainActivity_] 活动。
在 [MainActivity] 的代码中,我们添加以下 [@AfterViews] 方法:
@AfterViews
protected void afterViews() {
// recover intent if it exists
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// we retrieve the name
String nom = extras.getString("NOM");
if (nom != null) {
// we display it
editTextNom.setText(nom);
}
}
}
}
进行这些修改并测试您的应用程序。现在,当您从视图 2 返回视图 1 时,您最初输入的名称应该会显示出来,而此前并非如此。
![]() | ![]() |
1.6.7. Activity 生命周期
在第 1.3.5 节中,我们介绍了 Activity 的生命周期。这里我们有两个 Activity,并在运行过程中在它们之间切换。这两个 Activity 包含两个方法——[onCreate] 和 [afterViews]——但它们何时被调用、以及一个相对于另一个的调用顺序并不直观。了解这一点非常重要。为了弄清楚,我们将在两个 Activity 中添加日志:
因此,在 [MainActivity] 类中,我们编写:
// manufacturer
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity", "onCreate");
...
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
}
}
- 第 2–4 行:我们想知道 [MainActivity] 类是被实例化一次还是多次;
- 第 8 行:我们想知道 [onCreate] 方法是被调用一次还是多次;
- 第 14 行:我们想知道 [afterViews] 方法是被调用一次还是多次;
我们在 [SecondActivity] 类中也做了完全相同的事情。
当应用启动时,我们看到以下日志:
第一个活动的 [onCreate, afterViews] 方法按此顺序执行。当您点击 [View #2] 按钮时,新的日志如下:
第二个活动的 [onCreate, afterViews] 方法按此顺序执行。当您点击 [View #1] 按钮时,新的日志如下:
因此,[MainActivity] 类被再次实例化。当您点击 [View #2] 按钮时,新的日志如下:
因此,[SecondActivity] 类被再次实例化。
因此,每当活动发生切换时,这两个活动都会被系统性地重新创建。
现在,我们将探讨一种架构,其中单个 Activity 能够管理多个称为片段(fragments)的视图。与之前可能多次实例化 Activity 的方法不同,该 Activity 和视图将仅被实例化一次。
1.7. 示例-06:标签页导航
在此我们将探讨标签页界面。该示例虽然复杂,但涵盖了后续将用到的所有元素:单个 Activity、片段管理器(视图)、片段容器以及片段间的导航。标签页的概念与片段不同,且在本示例中属于次要内容。
1.7.1. 创建项目
我们创建一个新项目:
![]() | ![]() |
![]() |
![]() |
- 在 [7] 中,选择一个带标签的活动(Tabbed Activity);
![]() |
- 在 [10-14] 中,保留默认值;
- 在 [15] 中,选择带有标题栏的标签页;
生成的项目如下:
![]() | ![]() |
- 在[1]中,该活动;
- 在[2]中,这些观点;
一个以模块命名的运行时配置 [app] 已被自动创建 [2b]:
![]() |
您可以运行它。随后将出现一个带有三个选项卡的窗口 [3-6]:

1.7.2. Gradle 配置
该项目 [Example-06] 生成了以下 [build.gradle] 文件:
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.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'
}
与之前所见相比,这里新增了一个元素:第 25 行。生成的应用程序所使用的新组件需要此库。
1.7.3. [activity_main] 视图
![]() |
[activity_main] 视图是与项目中的 [MainActivity] 关联的视图。在 [设计] 模式下,该视图如下所示:

它包含以下组件:
![]() |
- [main_content] 是整个视图;
- [appbar](红色框 1)是应用程序栏。它包含两个组件:
- [toolbar](黄色框 4)是工具栏;
- [tabs](橙色框 5)是标签栏;
- [container](绿色框,2)可容纳各种片段。片段即视图。因此,同一个 Activity 可以在该容器中显示多个视图(片段);
- [fab](组件 3)被称为悬浮组件;
在 [文本] 模式下,代码如下:
<?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="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>
我们可以看到前面描述过的元素:
- 第 2–49 行:[main_content] 组件的定义(第 5 行),该组件构成了整个视图。我们可以看到它是一个 [CoordinatorLayout] 布局(第 2 行);
- 第 11–33 行:[appbar] 容器(第 12 行)。这是一个 [AppBarLayout](第 11 行);
- 第 18–24 行:[toolbar] 组件(第 19 行),类型为 [Toolbar](第 18 行);
- 第 28–31 行:[tabs] 容器(第 29 行)。这是一个类型为 [TabLayout] 的布局(第 28 行)。它将显示标签页标题;
- 第 35–39 行:[container] 组件(第 36 行)。该容器用于显示 Activity 的不同视图;
- 第 41–47 行:类型为 [FloatingActionButton](第 41 行)的 [fab] 组件(第 42 行)。这是一个可点击的按钮。默认情况下,它位于整个视图的右下角;
我们不会试图理解所有这些组件属性的含义。我们将直接使用它们。正是通过实践——通常是在 [设计] 模式下——我们才发现它们的作用。在此模式下,我们会发现组件拥有数十个属性。通常,只有部分属性会被初始化,其余则保留其默认值。
不过,让我们先澄清几点。配置不同视图的大部分值都集中在 [res/values] 文件夹中:
![]() |
这些值在 [activity_main.xml] 文件的第 15–16 行、第 23 行、第 39 行和第 46 行中被引用。让我们来看一个例子:
- 第 15 行:
android:paddingTop="@dimen/appbar_padding_top"
[@dimen] 注解指向 [res/values/dimens.xml] 文件:
<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>
[activity_main.xml] 文件的第 15 行对应上文中的 (f) 行;
同样,注解:
- [@string] 指向资源文件 [res/values/strings.xml];
- [@color] 指向资源文件 [res/values/colors.xml];
- [@style] 指向资源文件 [res/values/styles.xml];
1.7.4. 该 Activity
![]() |
为该 Activity 生成的代码与上述视图的复杂程度相匹配:它相当复杂。我们将分几个步骤进行分析。
1.7.4.1. 管理片段和标签页
[MainActivity] 中与片段和标签页相关的代码如下:
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 no. i in the fragment container is fragment no. i delivered 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 n° i displays fragment n° i of the container
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
}
// a fragment
public static class PlaceholderFragment extends Fragment {
...
}
// the fragment manager
// it is used to request fragments to be displayed in the main view
// must define methods [getItem] and [getCount] - the others are optional
public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
}
}
- 第 28 行:Android 提供了一个类型为 [android.support.v4.view.ViewPager] 的视图容器(第 12 行)。该容器必须配备一个视图或片段管理器。开发者负责提供该管理器;
- 第 25 行:本示例中使用的片段管理器。其实现位于第 61–63 行;
- 第 31 行:活动创建时执行的方法;
- 第 35 行:将 [activity_main.xml] 视图与该 Activity 关联;
- 第 37 行:通过标识符获取视图中 [toolbar] 组件的引用;
- 第 38 行:该工具栏成为活动的操作栏(Android 概念);
- 第 40 行:实例化片段管理器。构造函数的参数是 Android 类 [android.support.v4.app.FragmentManager](第 10 行);
- 第 44 行:我们通过 ID 从 [activity_main.xml] 视图中获取片段容器的引用;
- 第 45 行:将片段管理器与片段容器关联。这意味着当片段容器被要求显示片段 #i 时,它将向片段管理器请求该片段;
- 第 48 行:我们通过其标识符获取标签栏的引用;
- 第 49 行:将标签管理器与片段容器关联。这意味着当点击第 i 个标签时,容器将显示片段 #i。标签管理器与片段容器之间的关联消除了对标签管理的需求。因此,我们无需为点击标签定义事件处理程序。与片段容器的关联默认提供了此功能。 我们将看到一个片段数量多于标签数量的示例。在这种情况下,我们不会建立这种关联。
片段适配器 [SectionsPagerAdapter] 如下所示:
// the fragment manager
// it is used to request fragments to be displayed in the main view
// must define methods [getItem] and [getCount] - the others are optional
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// fragment n° position
@Override
public Fragment getItem(int position) {
// instantiate a fragment [PlaceHolder] and render it
return PlaceholderFragment.newInstance(position + 1);
}
// makes the number of fragments managed
@Override
public int getCount() {
return 3;
}
// optional - gives a title to 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;
}
}
}
- 应用显示的片段取决于应用本身。片段管理器由开发者定义;
- 第 5 行:片段管理器继承了 Android 类 [android.support.v4.app.FragmentPagerAdapter]。构造函数已为我们提供。我们必须至少定义以下两个方法:
- int getCount():返回待管理的片段数量;
- Fragment getItem(i):返回第 i 个片段;
CharSequence getPageTitle(i) 方法(返回第 i 个片段的标题)是可选的。由于标签页管理器已与片段管理器关联,因此第 i 个标签页的标题即为第 i 个片段的标题。因此,第 27–33 行中的标题将作为标签页标题;
- 第 18–21 行:getCount 返回所管理的片段数量,本例中为三个;
- 第 11–15 行:getItem(i) 返回片段 #i。此处所有片段均为同一类型,即 [PlaceholderFragment];
- 第 24–35 行:getPageTitle(int i) 返回片段 #i 的标题;
1.7.4.2. 显示的片段
![]() |
此处的活动片段均为同一类型,且均与以下 XML 视图 [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="exemples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- 第 1–16 行:一个 [RelativeLayout] 布局;
- 第 11–14 行:视图(片段)中的唯一组件:一个标识为 [section_label] 的 [TextView];
在 [MainActivity] 中,所管理的片段属于以下 [PlaceholderFragment] 类型:
// a fragment
public static class PlaceholderFragment extends Fragment {
// a text displayed in the fragment
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
}
// renders a fragment with one piece of information: the fragment number passed as a parameter
public static PlaceholderFragment newInstance(int sectionNumber) {
// fragment
PlaceholderFragment fragment = new PlaceholderFragment();
// on-board 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) {
// view [fragment_main] 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)));
// we return the view
return rootView;
}
}
- 第 2 行:[PlaceholderFragment] 类继承自 Android 的 [Fragment] 类。这通常是标准做法;
- 第 2 行:[PlaceholderFragment] 类是静态的。其 [newInstance] 方法(第 10 行)允许您获取 [PlaceholderFragment] 类型的实例;
- 第 10–19 行:[newInstance] 方法创建并返回一个 [PlaceholderFragment] 类型的对象;
- 第 14–16 行:该片段是通过传入参数创建的;
片段必须在第 22 行定义 [onCreateView] 方法。该方法必须返回与该片段关联的视图。
- 第 25 行:视图 [fragment_main.xml] 与该片段相关联;
- 第 27 行:该视图包含一个 [TextView] 组件,其引用是通过其 ID 获取的;
- 第 29 行:文本显示在 [TextView] 中;
- [getString] 是父类 [AppCompatActivity] 的方法;
- 第一个参数是组件 ID。[R.string.section_format] 指代文件 [res/values/strings.xml](下文第 4 行)中由 [section_format] 标识的组件 ID:
<resources>
<string name="app_name">Exemple-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
- (续)
- 上文第 (d) 行中的 %1$d 表示参数 #1 (%1) 必须格式化为整数 ($d);
- [getString] 的第二个参数是要赋值给上文第 (d) 行中参数 $1 的值;
- [getArguments] 返回片段参数包的引用。这里需要特别注意的是,每个参数都是在以下参数包中创建的(第 f-h 行):
// renders a fragment with one piece of information: the fragment number passed as a parameter
public static PlaceholderFragment newInstance(int sectionNumber) {
// fragment
PlaceholderFragment fragment = new PlaceholderFragment();
// on-board info
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// result
return fragment;
}
- (续)
- 因此,getArguments().getInt(ARG_SECTION_NUMBER) 将返回上文第 (g) 行和第 (b) 行中的 [sectionNumber] 值;
- 第 31 行:我们返回由此创建的视图;
1.7.4.3. 菜单管理
在生成的应用程序中,有一个菜单:
![]() |
[menu_main.xml] 文件的内容如下:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- 第 1-9 行:菜单;
- 第 5-8 行:一个由 [action_settings](第 5 行)标识的菜单项;
- 第 6 行:菜单选项的标签。该标签位于文件 [res/values/strings.xml] 中(见下文 (c) 行):
<resources>
<string name="app_name">Exemple-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
上面的代码对应以下界面(菜单位于 Android 运行时窗口的右上角):
![]() | ![]() |
在 [MainActivity] 活动中,该菜单的处理方式如下:
@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/Up button, so 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);
}
- 第 1–6 行:当系统准备创建应用程序菜单时,会调用此方法。输入参数 [Menu menu] 是一个尚未包含任何选项的空菜单;
- 第 4 行:使用文件 [res/menu/menu_main.xml]。作为参数传递的 [Menu menu] 对象被赋予了该文件中定义的菜单选项;
- 第 5 行:表明菜单已创建;
- 第 8–21 行:每当菜单选项被点击时,都会执行 [onOptionsItemSelected] 方法;
- 第 13 行:被点击的菜单选项的引用;
- 第 16–18 行:如果被点击的选项标识符为 [action_settings],则不执行任何操作,并标记该事件已处理(第 17 行);
- 第 20 行:将事件传递给父类;
为了更清楚地了解该菜单的运行情况,我们在之前的代码中添加了日志:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("menu", "création menu en cours");
// 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/Up button, so 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. 悬浮按钮
生成的视图包含一个悬浮按钮:
![]() |
该组件在主视图 [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"/>
第 7 行引用了 Android 框架提供的图像,具体来说是一个信封。
该组件在 [MainActivity] 类中按如下方式处理:
// 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();
}
});
- 第 2 行:获取与该 Activity(activity_main)关联的视图中浮动按钮的引用;
- 第 3–9 行:为其分配一个处理程序以处理点击事件;
- 第 6 行:[Snackbar] 类允许您使用其 [Snackbar.make] 方法在视图上显示临时消息。第一个参数是一个视图,[Snackbar] 将从中查找用于显示消息的父视图。此处,[view] 是被点击的信封的视图(第 5 行)。将被找到的父视图是 [activity_main] 视图。 第二个参数是要显示的消息。第三个参数是显示时长(SHORT 或 LONG);
- 第 7 行:您可以点击显示的消息以触发某个操作。此处,点击消息未关联任何操作。最后,[show] 方法会显示该消息;
点击浮动按钮会产生以下视觉效果:
![]() |
1.7.5. 运行项目
既然我们已经详细解释了生成的代码,现在可以更好地理解其执行过程:

当您点击标签页 #i 时,片段 #i 会显示在视图容器中。这从 [4] 中显示的文本中可以明显看出。您还可以看到,通过用鼠标向右或向左滑动视图,可以在标签页之间切换。我们将看到,这种行为是可以被控制的。
当您点击[6]处的菜单选项时,会看到以下日志:
![]() |
1.7.6. 片段生命周期
![]() | ![]() |
- 在[1]中,我们可以看到,当片段首次显示时,以及每次活动需要重新绘制片段时,[onCreateView]方法及其后续方法都会被执行;
为了追踪 Activity 和片段的生命周期,我们在 [MainActivity] 代码中添加了以下日志:
// manufacturer
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 {
// a text displayed in the fragment
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
// renders a fragment with one piece of 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)));
...
}
}
}
我们再次运行该项目。最初的日志如下:
- 第 1 行:创建 Activity;
- 第 2 行:执行其 [onCreate] 方法;
- 第 3-4 行:实例化片段 #1;
- 第 5-6 行:实例化片段 #2;
- 第 7 行:初始化片段 #2;
- 第 8 行:初始化片段 #1;
- 第 9 行:创建 Activity 菜单;
在此,我们需要回顾负责创建片段的代码:
// the fragment manager
// it is used to request fragments to be displayed in the main view
// must define methods [getItem] and [getCount] - the others are optional
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// fragment n° position
@Override
public Fragment getItem(int position) {
// instantiate a fragment [PlaceHolder] and render it
return PlaceholderFragment.newInstance(position + 1);
}
...
- 第 11–15 行:每当片段容器请求一个片段时,[newInstance] 都会实例化一个片段;
上述日志显示,前两个片段已实例化并初始化。
现在,让我们点击第 2 个标签页。新的日志如下:
- 第 1–3 行:片段 #3 被实例化并初始化。请记住,当前显示的是片段 #2;
现在,我们点击第 3 个标签页。这里没有日志。这很可能是因为待显示的第 3 个片段已经实例化了。现在,我们回到第 1 个标签页。日志如下:
片段 #1 并未被重新实例化,但其 [onCreateView] 方法被再次执行。其他两个片段也出现了同样的行为。
从这些日志中,我们可以得出以下结论:
- 该 Activity 被实例化并初始化了一次;
- 每个片段都被实例化了一次;
- 每个片段的 [onCreateView] 方法被执行了多次;
您需要了解——且日志也证实了这一点——默认情况下,当显示片段 #i 时,如果片段 i-1 和 i+1 尚未实例化,它们会被实例化。这解释了,例如,为什么在启动时,尽管应该显示片段 #1,但片段 1 和 2 却被实例化并初始化了。 日志还显示,即使显示了多次第 i 个片段,[getItem(i)] 方法也仅被调用一次。因此,似乎负责显示 [SectionsPagerAdapter] 的第 i 个片段的片段容器 [ViewPager],仅向片段管理器 [ ] 请求该片段一次。此后,它不再再次请求,而是继续使用已获取的那个片段。
最后,日志提供了关于片段 [onCreateView] 方法的信息:
- 在启动时,片段 1 和 2 被实例化,并执行了它们的 [onCreateView] 方法;
- 从片段 1 切换到片段 2 时,片段 2 的 [onCreateView] 方法并未被重新执行。 因此,无法利用该方法更新片段 2。然而,用户可能已在片段 1 中执行了某项操作,其结果本应由片段 2 显示。我们看到 [onCreateView] 方法无法用于更新片段 2。我们需要寻找其他解决方案;
1.8. 示例-07:使用 [AA] 库重写示例-06
1.8.1. 创建项目
我们将复制 [示例-06] 项目并重命名为 [示例-07],以便在后者中引入 Android 注解。为此,请按照第 1.4 节中的步骤操作。最终结果如下:
![]() | ![]() |
1.8.2. Gradle 配置
![]() |
我们将 [build.gradle] 文件更新如下:
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.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'
}
我们已添加了使用 [Android Annotations] 库所需的配置(参见第 1.4 节)。
1.8.3. 添加第一个 AA 注解
我们将在 [MainActivity] 中创建 AA 注解:
![]() |
[MainActivity] 类的更改如下:
@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;
// manufacturer
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 no. i in the fragment container is fragment no. i issued by the fragment manager
mViewPager.setAdapter(mSectionsPagerAdapter);
// the tab bar is also associated with the fragment container
// i.e. tab n° i displays fragment n° i of 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();
}
});
}
- 第 1 行:[@EActivity] 注解将 [MainActivity] 设为由 AA 管理的类。其参数 [R.layout.activity_main] 是与该 Activity 关联的 [activity_main.xml] 视图的标识符;
- 第 11-12 行:由 [R.id.tabs] 标识的组件被注入到 [tabLayout] 字段中。这是标签页管理器;
- 第 14–15 行:由 [R.id.fab] 标识的组件被注入到 [fab] 字段中。这是浮动按钮;
- 第 23–50 行:原先位于 [onCreate] 方法中的代码被移至一个任意命名的、但标注了 [@AfterViews] 的方法中(第 23 行)。在此类标注的方法中,我们可以确信所有标注了 [@ViewById] 的视觉界面组件均已初始化;
- 我们还添加了日志以查看该 Activity 的生命周期;
请记住,[@EActivity] 注解会生成一个 [MainActivity_] 类,该类将是项目的实际 Activity。因此,您必须按以下方式修改 [AndroidManifest.xml] 文件:
<?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_"
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>
- 第 12 行:新的 Activity。
此时,请再次运行项目,并确认是否仍能看到带有标签页的界面。
1.8.4. 重写片段
我们将回顾项目中片段的管理方式。目前,[PlaceholderFragment] 类是 [MainActivity] 活动的静态内部类。我们将回归更常见的用例,即片段定义在外部类中。此外,我们将为片段引入 AA 注解。
[Example-07] 项目的演变如下:
![]() |
在上文中,我们可以看到 [PlaceholderFragment] 类已被移出 [MainActivity] 类之外。其代码已重写如下:
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 no
private static final String ARG_SECTION_NUMBER = "section_number";
// manufacturer
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)));
}
}
}
- 第 15 行:该片段标注了 [@EFragment] 注解,其参数是与该片段关联的 XML 视图的标识符,在本例中即 [fragment_main.xml] 视图;
- 第 19-20 行:将 [fragment_main.xml] 中由 [R.id.section_label] 标识的组件引用注入到 [textViewInfo] 字段中,该组件的类型为 [TextView](见下文第 (l) 行):
<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="exemples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- 第 42–52 行:[onResume] 方法在与片段关联的视图显示之前执行。它可用于更新即将显示的用户界面;
- 第 47 行:必须调用父类中同名的方法;
- 第 49 行:尚不确定 [onResume] 方法是否会在第 20 行的字段初始化之前执行。用于跟踪片段生命周期的日志将告诉我们答案。目前,为谨慎起见,我们进行空值检查;
- 第 51 行:我们使用创建片段时传递给它的整数参数,更新 [textViewInfo] 字段中的信息;
[MainActivity] 类失去了其内部类 [PlaceholderFragment],其片段管理器演变如下:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private Fragment[] fragments;
// number of fragments
private static final int FRAGMENTS_COUNT = 3;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
// initialization of fragment table
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length; i++) {
// create a fragment
fragments[i] = new PlaceholderFragment_();
// you can pass arguments to the
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
}
// fragment n° position
@Override
public Fragment getItem(int position) {
Log.d("MainActivity", String.format("getItem[%s]", position));
return fragments[position];
}
// makes the number of fragments managed
@Override
public int getCount() {
return fragments.length;
}
// optional - gives a title to managed fragments
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
}
- 第 4 行:片段被放入数组中;
- 第16–23行:在构造函数中初始化片段数组。它们的类型是 [PlaceholderFragment_](第18行),而不是 [PlaceholderFragment]。 [PlaceholderFragment] 类确实已添加了 AA 注解,并将生成一个从 [PlaceholderFragment] 派生的 [PlaceholderFragment_] 类,而该活动必须使用的就是这个类。每个创建的片段都会接收一个整数参数,该参数将由片段显示;
- 第 42–45 行:我们已更改了片段标题。由于这些也是标签页标题,因此标签栏应显示相应变化;
现在让我们编译 [Make] [1] 该项目:
![]() | ![]() |
- 在 [2] 中,我们可以看到 AA 库生成的类位于 [app / build / generated / source / apt / debug] 文件夹中(您必须处于 [Project] 视图中才能看到 [2]);
运行 [Example-07] 项目并验证其是否仍能正常运行。
1.8.5. 查看日志
应用程序启动时,日志如下:
- 第 1 行:构建单个 Activity;
- 第 2 行:Activity 的 [afterViews] 方法:其带有 [@ViewById] 注解的字段被初始化;
- 第 3-5 行:三个片段的初始化;
- 第 6-7 行:片段容器 [ViewPager] 请求前两个片段;
- 第 8-9 行:片段 2 的方法;
- 第 10–11 行:片段 1 的方法;
- 第 12–13 行:片段 1 的 [onResume] 方法;
- 第 14–15 行:片段 2 的 [onResume] 方法;
- 第 16 行:创建 Activity 菜单;
请注意,这回答了之前提出的一个问题:例如片段 1 的 [onResume] 方法(第 12 行)会在该片段的 [afterViews] 方法(第 11 行)之后执行。因此,当 [onResume] 方法执行时,它可以访问带有 [@ViewById] 注解的字段。现在我们可以将 [onResume] 方法编写如下:
@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)));
}
现在让我们从标签页 1 切换到标签页 2。新的日志如下:
- 第 1 行:片段容器 [ViewPager] 请求片段 #3;
- 第 2-3 行:片段 #3 的方法。请注意,该片段在应用程序启动时已被实例化;
- 第 4-5 行:执行片段 #3 的 [onResume] 方法。请注意,当前显示的是片段 #2;
现在让我们从标签页 2 切换到标签页 3。此时没有日志输出。因此,片段 #3 的 [onCreateView、afterViews、onResume] 方法均未被执行。它能正确显示文本 [Hello World from section:3],仅仅是因为该文本已在之前显示片段 #2 的步骤中被创建。 回想一下,在那个步骤中,片段 #3 的 [onResume] 方法已被执行。我们可以看到,就像 [onCreateView] 方法一样,[onResume] 方法也不能用于更新片段 3。如果我们需要更改片段显示的文本,这两个方法都无法做到。
现在,让我们从标签页 #3 返回标签页 #1。此时日志如下:
我们可以看到 Fragment 1 中的所有方法均已执行。同时可以看到 getItem 方法并未被调用。如前所述,该方法对每个片段仅调用一次;
现在,让我们从标签页 1 切换到相邻的标签页 2。我们会得到以下日志:
令人惊讶,不是吗?片段 #3 的所有方法都被重新执行了。
要理解这些现象,请记住:默认情况下,当片段容器显示片段 i 时,它会初始化片段 i-1、i 和 i+1。让我们结合这一信息来回顾日志。
首先,应用启动时的日志:
由于片段容器将显示片段 1,因此片段 1 和 2 被初始化(第 8–15 行)。
现在,我们将从标签页 1 切换到标签页 2:
由于片段容器将显示片段 2,因此片段 1、2 和 3 必须被初始化。片段 1 和 2 已在上一步中完成初始化。片段 3 在第 2–5 行中进行初始化。
我们从标签页 2 切换到标签页 3。此时没有日志输出。由于片段容器将显示片段 3,因此片段 2 和 3 必须被初始化。然而,自上一步以来,它们已经初始化过了。我们在这里没有看到的是,片段 1(它与片段 3 不相邻)会丢失其状态,该状态不会保存在内存中。
我们从标签页 3 切换到标签页 1。日志如下:
由于片段容器将显示片段 1,因此片段 2 也必须被初始化。它已在上一步中完成初始化。在同一步骤中,片段 1 的状态已丢失。因此,在第 1–4 行中对其进行了重置。这里未显示的是,片段 3(它与片段 1 不相邻)会丢失其状态,该状态随后将不会保存在内存中。
当从标签页 1 切换到相邻的标签页 2 时,我们会得到以下日志:
由于片段容器将显示片段 2,因此片段 1、2 和 3 必须被初始化。片段 1 和 2 已在上一步中初始化。片段 3 在第 1–4 行中被初始化。
我们学到了什么?
- 默认的片段管理机制非常特殊,若不想为此抓狂,就必须理解它。我们可以更改这种管理模式,稍后我们将进行演示;
- 在默认处理机制下,[onCreateView、onResume] 方法均无法用于更新待显示的片段,因为我们无法确保它们会被执行;
1.8.6. onDestroyView
[onDestroyView] 方法是片段生命周期的一部分(参见第 1.7.6 节):
![]() | ![]() |
我们可以看到,在片段的生命周期中:
- [onCreateView] 方法可能会被执行多次;
- 在稍后返回 [onCreateView] 方法之前,必然会调用 [onDestroyView] 方法 [2];
我们将这些方法插入到片段中,以便更好地追踪其生命周期。片段代码如下:
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();
}
}
让我们运行该应用程序。最初的日志如下:
- 第 1 行:构建单个 Activity;
- 第 2 行:Activity 的 [afterViews] 方法:其带有 [@ViewById] 注解的字段被初始化;
- 第 3-5 行:三个片段的初始化;
- 第 6-7 行:片段容器 [ViewPager] 请求前两个片段;
- 第 8-9 行:创建片段 2 的视图(不一定显示出来);
- 第 10–11 行:创建片段 1 的视图(不一定显示出来);
- 第 12–13 行:片段 1 的 [onResume] 方法;
- 第 14-15 行:片段 2 的 [onResume] 方法;
- 第 16 行:创建 Activity 菜单;
从标签页 1 切换到标签页 3:
06-03 02:50:02.685 2346-2346/exemples.android D/MainActivity: getItem[2]
06-03 02:50:02.685 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: afterViews 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 02:50:03.024 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 1
- 第 1 行:片段容器请求第三个片段;
- 第 2-3 行:创建了片段 3 的视图(不一定显示);
- 第 4-5 行:执行片段 3 的 [onResume] 方法;
- 第 6 行:执行片段 1 的 [onDestroyView] 方法。这意味着当用户返回片段 1 或相邻片段时,该片段的生命周期将重新执行;
从标签页 3 返回标签页 1:
06-03 02:53:46.255 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:53:46.604 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 3
- 第 1–4 行:由于 Fragment 1 执行了 [onDestroyView],因此其生命周期被重新执行;
- 第 5 行:现在将执行片段 3 的 [onDestroyView] 方法。同样,当用户返回片段 3 或相邻的片段时,该片段的生命周期将重新执行;
1.8.7. setUserVisibleHint
生命周期的 [onCreateView] 方法会实例化与片段关联的视图,但并不一定使其可见。这就是我们接下来要探讨的内容。每当片段的可见性发生变化时,[Fragment.setUserVisibleHint] 方法都会被调用。我们将此方法添加到片段的代码中:
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));
}
}
启动时,日志如下:
06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
- 第 7 行、第 9–10 行的日志显示,只有 Fragment 1 变得可见。我们还可以看到,它在 [onCreateView] 方法执行之前就变得可见;
现在让我们从标签页 1 切换到标签页 2:
06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
- 片段 1 被隐藏(第 3 行),片段 2 被显示(第 4 行);
现在从标签页 2 切换到标签页 3:
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
- 片段 2 被隐藏(第 1 行),片段 3 被显示(第 2 行);
让我们回到标签页 1:
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
- 片段 3 被隐藏(第 2 行),片段 1 被显示(第 3 行);
我们学到了什么?
- 对于即将显示的片段,[setUserVisibleHint] 方法会在 [isVisibleToUser] 属性设置为 true 时执行一次;
- 我们无法确定该方法在片段生命周期中的具体执行时机。因此,对于片段 1,[setUserVisibleHint, true] 方法是在该片段生命周期开始时 [onCreateView] 方法之前执行的,而对于片段 2 和 3,情况则恰恰相反;
1.8.8. setOffscreenPageLimit
前面的日志显示,当片段容器 [ViewPager] 即将显示第 i 个片段时,如果尚未执行,它会执行相邻片段 i-1 和 i+1 的生命周期。可以通过 [ViewPager].setOffscreenPageLimit 方法控制这种行为:
通过上述指令,
- 当片段容器 [ViewPager] 即将显示第 i 个片段时,如果尚未执行,它将执行范围 [i-n, i+n] 内相邻片段的生命周期;
- 如果随后显示片段 j:
- 区间 [j-n, j+n] 内的相邻片段也会发生同样的现象;
- 在步骤 1 中初始化的片段,若不再与 [j-n, j+n] 范围内的片段相邻,则可能触发 [onDestroyView] 操作。然而,我在其他应用中(特别是第 3 章中的那个)观察到,情况并非总是如此;
我们将 [MainActivity.afterViews] 方法修改如下:
@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 no. i in the fragment container is fragment no. i delivered by the fragment manager
mViewPager.setAdapter(mSectionsPagerAdapter);
// inhibit 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 n° i displays fragment n° i of 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();
}
});
}
- 第 20 行:我们将初始化相邻片段的数量设置为片段总数减 1。 因此,在启动时,当片段容器显示第 1 个片段时,它将同时初始化片段 2、3、...、n,其中 n = 1 + mSectionsPagerAdapter.getCount() - 1 = mSectionsPagerAdapter.getCount()。这意味着所有片段都将被初始化。当视口移动到另一个片段时,片段容器:
- 会检测到所有与新片段相邻的片段均已初始化,因此不会再次初始化它们;
- 由于新片段的相邻范围也涵盖了所有片段,因此片段容器不会“取消初始化”任何片段;
总而言之,我们应看到所有片段在应用程序启动时被实例化并初始化,此后便不再进行初始化。接下来我们将通过检查日志来验证这一点。
启动时,我们有以下日志:
- 第4–6行:构建三个片段;
- 第7、9、11行:片段容器请求这三个片段。在之前的版本中,它请求的是两个;
- 第14–25行:三个片段的生命周期运行;
现在让我们从标签页 1 切换到标签页 2:
让我们从标签页 2 切换到标签页 3:
然后从标签页 3 切换到标签页 1:
日志证实了这一推测。所有片段都在启动时被实例化并初始化。此后,它们的生命周期方法不再被执行。这是片段的一种非常可预测的行为,这也使得它们的使用变得更加简单。
我们需要找到一种方法,无论开发者选择了哪种片段邻接模式,都能在片段即将显示时对其进行更新。日志向我们揭示了两点:
- 对于即将显示的片段,[setUserVisibleHint, true] 方法总是会被执行,但其他片段则不会;
- 该事件可能发生在片段生命周期事件之前或之后,这取决于开发者选择的片段邻接关系。这会造成问题,因为如果生命周期事件尚未发生,就意味着无法通过 [setUserVisibleHint, true] 方法更新该片段;
当片段邻接关系为 1 时,应用程序启动时的日志如下:
06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
- 我们可以看到,当 Fragment 1 可见时,其视图尚未创建。因此,我们无法与其交互。这可以在片段的生命周期中完成,例如在 [onCreateView] 方法(第 11 行)或 [onResume] 方法(第 13–14 行)中。 由于我们使用了 AA 注解,通常无需编写 [onCreateView] 方法。因此,[onResume] 方法似乎是此处更新 Fragment 1 的最合适选择;
当我们在标签页 1 和标签页 2 之间切换时,日志如下:
06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
这次,我们仅在第 4 行使用了 [setUserVisibleHint, true] 方法来更新片段 2;
当我们在标签页 2 和标签页 3 之间切换时,日志如下:
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
这里,我们仅在第 2 行通过 [setUserVisibleHint, true] 方法来更新片段 3;
当我们在标签页 3 和标签页 1 之间切换时,日志如下:
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
在此,您必须使用 Fragment 1 的 [onResume] 方法(第 6–7 行)来更新 Fragment 1。
因此,在这个示例中,我们可以看到,要更新即将显示的片段,有两种方法:[setUserVisibleHint] 和 [onResume]。
我们将在此新项目中实现该解决方案,该项目中每个片段必须显示其已被显示的次数,我们将此称为“访问”。因此,每次显示时都需要更新其显示内容。这正是我们试图解决的问题。
在此之前,让我们先探讨一下 Activity 或 Fragment 生命周期的最后阶段:被销毁时。如果其他优先级更高的 Activity 需要当前不可用的资源,系统可能会决定销毁某个 Activity。为了释放这些资源,系统会主动销毁某些 Activity。此时,Activity 和 Fragment 的 [onDestroy] 方法将会被调用。
1.8.9. OnDestroy
![]() | ![]() | ![]() |
我们将允许用户通过菜单选项 [5] 删除该 Activity。为此,我们在 [menu_main.xml] 文件 [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>
只需复制并粘贴第一个菜单选项,然后调整结果(第 9 行和第 10 行)。此新选项的标签已添加到 [strings.xml] 文件中 [2]:
<resources>
<string name="app_name">Exemple-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>
最后,在 [MainActivity] 类中,我们处理 [Terminate] 选项的点击事件:
@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, so 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");
//we finish the activity
finish();
return true;
}
// parent
return super.onOptionsItemSelected(item);
}
- 第 14–19 行:复制并粘贴第 10–13 行,并将代码调整为适用于新选项;
- 第 17 行:该 Activity 因软件操作而终止;
现在让我们运行这个新版本,并在第一个视图显示后立即点击 [终止] 菜单选项。此时日志如下:
- 第 1-2 行:点击 [Terminate] 选项;
- 第 4 行:调用 Activity 的 [onDestroy] 方法;
- 第 4-5 行:调用片段 1 的 [onDestroyView] 方法,随后调用其 [onDestroy] 方法;
- 第 6-9 行:此过程对另外两个片段重复进行;
需要记住的是,当活动即将被系统、开发者或用户销毁时,会调用活动和片段的 [onDestroy] 方法。该方法可用于保存信息(例如保存在平板电脑的本地存储中),以便用户重新启动应用时能够检索这些信息。
1.9. 示例-08:根据片段邻接关系更新片段
1.9.1. 创建项目
将 [示例-07] 项目复制为 [示例-08]。操作步骤与第 1.4 节中将 [示例-02] 复制为 [示例-03] 的方法相同。
![]() | ![]() |
1.9.2. 重写 [PlaceholderFragment] 片段
[PlaceholderFragment] 片段的新代码如下。无论片段被分配了何种邻接关系(1、部分、完全),该代码均可正常运行:
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 no
private static final String ARG_SECTION_NUMBER = "section_number";
// manufacturer
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));
// init done
initDone = true;
}
// current text display
textViewInfo.setText(text);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
@Override
public void onDestroyView() {
...
}
@Override
public void onResume() {
...
}
// update fragment
public void update() {
// 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()));
// modified text
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// local info for logs
private String getInfos() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s", numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone);
}
}
- 第 34–48 行:[@AfterViews] 方法可能会被执行多次。我们以前曾用它来初始化片段的文本(第 42 行)。我们仍然这样做,但为了确保它只发生一次,我们管理一个布尔变量 [initDone](第 44 行),以指示初始化已完成且无需重复;
- 第 56–59 行:我们引入了 [onDestroyView] 方法,以应对片段下次重新显示时其生命周期将重新执行的情况;
- 日志显示,[@AfterViews] 方法之后可能执行两个方法:[setUserVisibleHint] 和 [onResume]。其中 [onResume] 方法仅在片段生命周期被执行时才会被调用。然而,[setUserVisibleHint] 方法并不总是在 [@AfterViews] 方法之后执行。 日志显示,这两个方法中至少有一个会在 [@AfterViews] 方法之后执行。日志从未显示过这两个方法会在 [@AfterViews] 方法之后同时执行。要么是其中一个,要么是另一个。作为预防措施,当更新完成后,我们将设置一个布尔变量 [updateDone];
[setUserVisibleHint] 和 [onResume] 方法如下:
// data
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 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;
}
}
- 第 14 行:存储片段的可见状态;
- 第 22–25 行:如果片段可见且 [@AfterViews] 方法已执行,则执行 [update] 方法并将布尔值 [updateDone] 设为 true;
- 第 26–28 行:如果片段即将被隐藏,则将布尔值 [updateDone] 重置为 false。 我们需要一个事件来将布尔变量 [updateDone](该变量在 [update] 方法被调用时立即被设为 true)重置为 false,以便进行新的更新。我们利用片段不再可见这一事实来实现这一点。当片段再次可见时,必须再次对其进行更新;
- 第 32–42 行:日志显示,根据为片段选择的邻接关系,即使片段不可见,[onResume] 方法也可能执行。如果片段不可见,我们不执行更新(第 39 行),并且与 [setMenuVisibility] 的处理方式一样,我们管理布尔变量 [updateDone]。
最后,[onDestroyView] 方法如下:
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
// indicator update
afterViewsDone = false;
// log
Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}
当片段的生命周期结束时,[onDestroyView] 方法会被执行。该片段的生命周期可能会在稍后恢复。
- 第 6 行:[onDestroyView] 方法会移除与该片段关联的视图的所有连接。该视图将在片段的下一次生命周期中重新创建。目前,我们需要将布尔值 [afterViews] 设置为 false,以表明与该视图的连接已不存在;
我们将运行一个包含 5 个片段且相邻度为 2 的应用程序。相关更改在 [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);
...
}
启动日志如下:
05-31 06:23:07.015 32551-32551/exemples.android D/MainActivity: constructor
05-31 06:23:07.041 32551-32551/exemples.android D/MainActivity: afterViews
05-31 06:23:07.050 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[0]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[1]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[2]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.280 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.291 32551-32551/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.294 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.798 32551-32551/exemples.android D/menu: création menu en cours
- 第 8、10、12 行:片段容器请求了与片段 1 相邻的所有片段;
- 第 9、11、13 行:这些片段的 [setUserVisibleHint] 方法被调用,且 [visibleToUser] 被设置为 false;
- 第 14 行:调用片段 1 的 [setUserVisibleHint] 方法,并将 [visibleToUser] 设为 true;
- 第 15–17 行:调用 3 个相邻片段的 [afterViews] 方法。此处可见该方法是在片段可见后(片段 1,第 14 行)被调用的;
- 第 18–20 行:调用 3 个相邻片段的 [onResume] 方法;
从标签页 1 切换到标签页 2:
05-31 06:52:36.132 32551-32551/exemples.android D/MainActivity: getItem[3]
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
- 由于片段布局向右偏移了一个位置,片段 4 被片段容器占用;
- 第 2 行:调用片段 4 的 [setUserVisibleHint] 方法,并将 [visibleToUser] 设置为 false;
- 第 3 行:调用片段 1 的 [setUserVisibleHint] 方法,并将 [visibleToUser] 设为 false。结果,片段 1 现在被隐藏;
- 第 4 行:调用片段 2 的 [setUserVisibleHint] 方法,并将 [visibleToUser] 设为 true。片段 2 现在可见;
- 第 5-6 行:片段 4 的生命周期继续;
我们从标签页 2 切换到标签页 3:
05-31 06:58:16.228 32551-32551/exemples.android D/MainActivity: getItem[4]
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
- 由于片段布局向右偏移了一个位置,片段 5 被片段容器占用;
- 第 2 行:调用片段 5 的 [setUserVisibleHint] 方法,并将 [visibleToUser] 设置为 false;
- 第 3 行:调用片段 2 的 [setUserVisibleHint] 方法,并将 [visibleToUser] 设为 false。结果,片段 2 现在被隐藏;
- 第 4 行:调用片段 3 的 [setUserVisibleHint] 方法,并将 [visibleToUser] 设为 true。片段 3 现在可见;
- 第 5-6 行:片段 5 的生命周期继续;
我们从标签页 3 切换到标签页 4:
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- 第 1 行:片段 3 现已隐藏;
- 第 2 行:片段 4 现已可见。请注意,片段 4 的生命周期并未被执行。这已在两步之前完成;
- 第 3 行:片段 1 离开了显示中的片段 4 的邻近区域。其 [onDestroyView] 方法被执行。下次显示时,其视图生命周期 [onCreateView、afterViews、onResume] 将被重新执行;
我们从标签页 4 切换到标签页 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/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- 第 1 行:片段 4 现已隐藏;
- 第 2 行:片段 5 现已可见。请注意,片段 5 的生命周期方法并未在此处执行。该操作已在两步之前完成;
- 第 3 行:片段 2 离开显示中的片段 5 附近。其 [onDestroyView] 方法被执行;
我们从第 5 个标签页切换到第 1 个标签页:
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 4 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 5 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- 第 1、4、5、6 行:片段 1 的生命周期被重新执行。这是因为它与视图的连接已断开;
- 第 2、5、8、9 行:出于相同原因,片段 2 的生命周期被重新执行;
- 第 10–11 行:片段 4 和 5 被移出显示片段的邻近区域;
- 第 7 行:片段 1 被更新;
![]() |
日志从未显示 [setUserVisibleHint] 和 [onResume] 方法都试图更新片段。二者必居其一。建议读者进行进一步测试并监控日志,以充分理解片段邻接关系和生命周期的概念。
现在,让我们设置完全邻接关系,并运行相同的测试。
在 [MainActivity] 中:
// number of fragments
private final int FRAGMENTS_COUNT = 5;
// fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;
启动日志如下:
05-31 07:34:44.717 28908-28908/exemples.android D/MainActivity: constructor
05-31 07:34:44.844 28908-28908/exemples.android D/MainActivity: afterViews
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[0]
05-31 07:34:45.201 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[1]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[2]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[3]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/MainActivity: getItem[4]
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.207 28908-28908/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.209 28908-28908/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:46.548 28908-28908/exemples.android D/menu: création menu en cours
- 日志显示 5 个片段的生命周期正在执行;
- 第 18 行显示了片段 1;
从标签页 1 切换到标签页 2:
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- 第 1 行:片段 1 被隐藏;
- 第 2 行:显示片段 2;
从标签页 2 切换到标签页 3:
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- 第 1 行:片段 2 被隐藏;
- 第 2 行:显示片段 3;
从标签页 3 切换到标签页 4:
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- 第 1 行:片段 3 被隐藏;
- 第 2 行:显示片段 4;
从标签页 4 切换到标签页 5:
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- 第 1 行:片段 4 被隐藏;
- 第 2 行:显示片段 5;
我们从第5个标签页切换到第1个标签页:
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- 第 1 行:片段 5 被隐藏;
- 第 2 行:显示片段 1;
- 第 3 行:片段 1 已更新;
从标签页 1 切换到标签页 4:
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: update 4 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- 第 1 行:片段 1 被隐藏;
- 第 2 行:显示片段 4;
- 第 3 行:片段 4 已更新;
我们可以看到,在完全相邻的情况下,片段的行为要可预测得多。
现在,我们将邻接度设为零,看看会发生什么。MainActivity 类的演变如下:
// number of fragments
private final int FRAGMENTS_COUNT = 5;
// fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = 0;
启动日志如下:
06-01 03:11:52.068 5679-5679/exemples.android D/MainActivity: constructor
06-01 03:11:52.353 5679-5679/exemples.android D/MainActivity: afterViews
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[0]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[1]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.571 5679-5679/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
06-01 03:11:54.597 5679-5679/exemples.android D/menu: création menu en cours
- 在第 8 行和第 10 行,我们可以看到片段容器请求了 2 个片段,即编号 1 和 2。因此,一切都按邻接度为 1 的情况进行。邻接度为 0 的情况因此被忽略。
1.9.3. 片段间通信
在之前的架构中,我们有一个 Activity 和 n 个片段。用户与各个片段进行交互。这些交互会改变应用程序的状态。这里,应用程序的状态指的是它在整个生命周期中存储的一组信息。于是,以下问题随之产生:
- 当用户与片段 i 交互时,应用程序从状态 E1 过渡到状态 E2;
- 用户在片段 i 上的操作导致片段 j 被显示;
- 如何将应用程序的当前状态 E2 同步到片段 j?
从之前的示例中,我们知道如何更新片段 j。但我们该从何处获取应用程序的状态 E2 来更新它呢?
这个问题有不同的解决方案。我们已经看到其中一种:片段 i 可以通过参数将应用程序的状态 E2 传递给片段 j。我们在 [MainActivity] 类中创建片段时就遇到过这种方法:
for (int i = 0; i < fragments.length; i++) {
// create a fragment
fragments[i] = new PlaceholderFragment_();
// you can pass arguments to the
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
此解决方案在此处无法直接使用。事实上,当用户点击标签页 j(这将显示片段 j)时,我们的代码并未被调用。此时仅执行系统代码。我们将在未来的项目中探讨如何拦截标签页点击事件,但目前我们将采取另一种方法。
我们已经讨论过应用程序的状态:即应用程序随时间推移所管理的一组数据。在此,应用程序由一个 Activity 和 n 个片段组成,它们都在应用程序启动时实例化一次,且其生命周期与应用程序的生命周期一致。 因此,这些元素中的任何一个——或其中几个组合——都可以作为存储应用程序状态的候选对象。每个片段都可以通过 [Fragment.getActivity()] 方法访问创建它的 Activity。既然所有片段都能访问该 Activity,将应用程序状态存储在其中似乎是理所当然的。
然而,[Fragment.getActivity()] 方法的返回结果取决于它在生命周期中的调用时机。我们通过在 [PlaceholderFragment] 类中添加几条日志来说明这一点:
// 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()));
// modified text
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// local info for logs
private String getInfos() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s, getActivity()==null:%s",
numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone, getActivity() == null);
}
- 第 14-16 行:[getInfo] 方法显示应用状态的一部分;
我们以片段邻接度为 2 的方式启动应用。应用启动时的日志如下:
06-01 03:26:13.769 10931-10931/exemples.android D/MainActivity: constructor
06-01 03:26:13.856 10931-10931/exemples.android D/MainActivity: afterViews
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:14.535 10931-10931/exemples.android D/MainActivity: getItem[0]
06-01 03:26:14.538 10931-10931/exemples.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/exemples.android D/MainActivity: getItem[1]
06-01 03:26:14.538 10931-10931/exemples.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/exemples.android D/MainActivity: getItem[2]
06-01 03:26:14.538 10931-10931/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.android D/menu: création menu en cours
- 第 9、10、13、14 行:我们可以看到,在 [setUserVisibleHint] 方法中,如果片段尚未可见(isVisibleToUser==false),则 [getActivity()==null];
- 第 19 行:当执行流程到达片段 1 的 [update] 方法时,[getActivity] 方法正确地返回了该 Activity;
当片段邻接关系设置为 4(完全邻接)时,日志如下:
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/exemples.android D/MainActivity: afterViews
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.002 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[0]
06-01 03:35:24.207 2814-2814/exemples.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/exemples.android D/MainActivity: getItem[1]
06-01 03:35:24.207 2814-2814/exemples.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/exemples.android D/MainActivity: getItem[2]
06-01 03:35:24.207 2814-2814/exemples.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/exemples.android D/MainActivity: getItem[3]
06-01 03:35:24.207 2814-2814/exemples.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/exemples.android D/MainActivity: getItem[4]
06-01 03:35:24.207 2814-2814/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.android D/menu: création menu en cours
我们得到了相同的结果。我们可以得出结论:一旦片段可见,[getActivity] 方法就会返回该片段的活动。我们还注意到,当执行到达即将显示的片段的 [update] 方法时,[getActivity] 方法确实返回了一个值。
为了演示片段间的通信,我们将创建一个新项目。
1.10. 示例-09:片段间通信、滑动和滚动
1.10.1. 创建项目
我们将 [Example-07] 项目复制为 [Example-08]。为此,我们将按照第 1.4 节中关于将 [Example-02] 复制为 [Example-03] 的步骤进行操作。
![]() | ![]() |
1.10.2. 会话
在这个新项目中,我们希望各个片段能显示用户已查看的片段总数。为此,我们需要维护一个所有片段均可访问的计数器。我们将封装片段共享数据的对象称为“会话”。这一术语源自Web开发领域,在该领域中,同一用户请求的不同视图之间需要共享的数据会被存放在会话中。 将不同片段共享的信息封装到一个对象中,能使代码更易于阅读。
[Session] 类将如下所示:
![]() |
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;
}
}
- 第 8 行:会话将封装已访问的片段数量;
- 第 5 行:[EBean] 注解是 AA 注解。其 [scope] 属性指定了被注解类的范围(或生命周期)。此处,[scope = EBean.Scope.Singleton] 属性将 [Session] 类设为单例:该类将在应用程序启动时被实例化一次且仅实例化一次。 随后,可以将带有 [EBean] 注解的类的引用注入到另一个类中。这就是依赖注入的概念;
1.10.3. [MainActivity]
[MainActivity] 活动的演变过程如下:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
...
// injection session
@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);
}
...
- 第 7-8 行:使用 [@Bean] 注解注入会话单例的引用。该注解的参数是要被注入的 Bean 的类。以这种方式注解的字段不能具有 [private] 作用域;
- 第 15 行:使用 [@AfterInject] 注解指定在该类的所有注入完成后调用的方法。因此,当进入第 16 行的 [afterInject] 方法时,第 8 行中的引用已经初始化完毕;
- 第 20 行:将访问计数器重置为零;
1.10.4. [PlaceholderFragment] 片段
[PlaceholderFragment] 片段的演变过程如下:
@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 no
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- 第 7 行:会话;
- 第 35-37 行:我们知道,当进入 [update] 方法时,[getActivity] 方法会正确返回活动。我们借此机会获取会话并将其本地存储(第 36 行);
- 第39–41行:为了递增访问次数,我们从会话中获取该值。 我们本可以将这段代码放在第19行开始的[setUserVisibleHint]方法中,因为我们知道在该处[getActivity]方法会返回活动。但在此,我们决定不为该方法赋予特定职责,而是将片段相关的代码移至片段的[update]方法中,该方法正是为此目的而设计的;
- 第 43 行:显示访问次数;
当运行包含 5 个片段(其中 2 个片段相邻)的应用程序时,初始日志如下:
05-31 08:38:47.305 20114-20114/exemples.android D/MainActivity: constructor
05-31 08:38:47.307 20114-20114/exemples.android D/MainActivity: afterInject
05-31 08:38:47.351 20114-20114/exemples.android D/MainActivity: afterViews
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
...
- 第 2–3 行:我们可以看到,该 Activity 的 [afterInject] 方法在其 [afterViews] 方法之前执行;
欢迎读者测试这个新应用程序。
1.10.5. 禁用滑动
在之前的应用中,当您使用鼠标在 Android 模拟器上向左或向右滑动时,当前视图会相应地被右侧或左侧的视图替换。这种默认行为并不总是理想的。我们将学习如何禁用视图滑动。
让我们回到主 XML 视图 [activity_main]:
![]() |
在视图的 XML 代码中,我们找到了片段容器:
<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"/>
第 1 行指定了管理该 Activity 页面的类。该类位于 [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;
...
第 12 行:片段容器类型为 [android.support.v4.view.ViewPager](第 1 行)。要禁用滑动,我们需要如下扩展该类:
![]() |
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 {
// swipe control
private boolean isSwipeEnabled;
// manufacturers
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// methods to be redefined to manage swiping
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// swipe allowed?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// swipe allowed?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// setter
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
}
- 第 8 行:[MyPager] 类继承自 Android 的 [ViewPager] 类(第 4 行);
- 当用手指滑动时,第 24 行和第 34 行的事件处理程序会被调用。这两个方法都返回一个布尔值。只需返回布尔值 [false] 即可禁用滑动;
- 第 11 行:用于指示是否允许滑动手势的布尔值。
完成上述设置后,我们需要使用新的页面管理器。这需要在 XML 视图 [activity_main.xml] 和主 Activity [MainActivity] 中进行。在 [activity_main.xml] 中,我们编写:
![]() |
<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"/>
第 1 行:我们使用新类。在 [MainActivity] 中,代码更改如下:
package exemples.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 no. i in the fragment container is fragment no. i issued by the fragment manager
mViewPager.setAdapter(mSectionsPagerAdapter);
// inhibit swiping between fragments
mViewPager.setSwipeEnabled(false);
// the tab bar is also associated with the fragment container
...
- 第 12 行:页面管理器现在类型为 [MyPager];
- 第 23 行:我们启用或禁用滑动功能。
测试此新版本。启用或禁用滑动功能,并观察当您使用鼠标将视图向右或向左拖动时,视图行为的差异。在所有未来的应用程序中,滑动功能将被禁用。我们不再提及此功能。
1.10.6. 禁用片段间的滚动
接下来我们继续改进标签页管理器。当从标签页 1 切换到标签页 4 时,你会看到中间的两个标签页(2 和 3)滚动而过。在 Android 术语中,这被称为 smoothScrolling。如果标签页数量较多,这种行为可能会令人烦躁。可以通过在片段管理器 [MyPager] 中添加以下代码来禁用它:
// swipe control
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;
}
由于标签页管理器已与片段管理器 [MyPager] 关联,当点击第 i 个标签页时,片段容器会通过上述 [setCurrentItem] 方法(第 9 行)显示第 i 个片段。[position] 是要显示的片段编号;
- 第 10 行:调用父类的 [setCurrentItem] 方法。将第二个参数设为 [false] 表示要求旧片段与新片段之间立即切换(不滚动);设为 [true] 则要求通过滚动过渡。此处,第二个参数是第 4 行字段的值,开发者可通过第 16–18 行中的方法设置该字段;
若要禁用滚动,[MainActivity] 类的代码将如下所示:
...
// fragment offset
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// inhibit swiping between fragments
mViewPager.setSwipeEnabled(false);
// no scrolling
mViewPager.setScrollingEnabled(false);
...
再次运行项目,并验证例如在标签页 1 和 4 之间不再有滚动。从现在开始,我们将始终禁用滚动。我们不会再讨论这个问题。
1.10.7. 一个新的片段
在本示例中,所有片段均为同一类型 [PlaceHolderFragment]。接下来我们将学习如何创建一个新的片段并将其显示出来。
首先,将 [Example-04] 项目中的视图 [vue1.xml] 复制到 [Example-09] 项目中 [1]:
![]() | ![]() |
- 在 [1] 中,视图 [vue1.xml];
- 在 [3] 中,视图显示了由于 [res/values/strings.xml] 文件中缺少文本而导致的错误;
在 [2] 中,我们从 [Example-04] 项目的 [res/values/strings.xml] 文件中提取缺失的文本并进行补充
<resources>
<string name="app_name">Exemple-07</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- vue 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
</resources>
- 在上文中,我们添加了第 6–9 行;
现在,我们创建 [Vue1Fragment] 类,该类将作为负责显示 [vue1.xml] 视图的片段:
![]() |
[Vue1Fragment] 类将如下所示:
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 {
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// event manager
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- 第 10 行:[@EFragment] 注解确保活动使用的片段实际上是 [Vue1Fragment_] 类。请牢记这一点。该片段与 [vue1.xml] 视图相关联;
- 第 14–15 行:由 [R.id.editTextNom] 标识的组件被注入到第 15 行的 [editTextNom] 字段中;
- 第 18–20 行:[doValider] 方法处理由 [R.id.buttonValider] 标识的按钮上的 'click' 事件;
- 第 21 行:[Toast.makeText] 的第一个参数类型为 [Activity]。方法 [Fragment.getActivity()] 用于获取包含该片段的活动。在此架构中,由于仅有一个活动负责显示不同的视图或片段,因此该活动即为 [MainActivity];
在 [MainActivity] 类中,片段管理器的实现如下:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private Fragment[] fragments;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
// initialization of fragment table
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
// create a fragment
fragments[i] = new PlaceholderFragment_();
// you can pass arguments to the
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
// a fragment of +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
...
}
- 第 13 行:共有 [FRAGMENTS_COUNT] 个片段:[FRAGMENTS_COUNT-1] 个类型为 [PlaceholderFragment] 的片段(第 14-21 行)和一个类型为 [Vue1Fragment_] 的片段(第 23 行,注意下划线);
编译并运行 [Example-09] 项目。第 5 页应呈现不同外观:
![]() |
1.10.8. 让所有片段都继承自同一个抽象类
新的 [Vue1Fragment] 片段在显示时也需要更新自身。为此,我们需要编写与 [PlaceholderFragment] 片段类似的代码。为了避免重复,我们将可提取的部分提炼为一个抽象类,应用程序中的所有片段都将继承自该类。
为此,我们创建一个新项目。
1.11. 示例-10:让所有片段继承自一个抽象类
1.11.1. 创建项目
我们将 [示例-09] 项目复制为 [示例-10]:
![]() | ![]() |
1.11.2. 调试模式管理
我们在项目中添加了一个选项,用于显示或隐藏调试模式日志。为此,我们在 [MainActivity] 类中添加了一个静态常量:
// mode debug
public static final boolean IS_DEBUG_ENABLED = false;
1.11.3. 所有片段的抽象父类
![]() |
[AbstractFragment] 类如下所示:
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 daughter classes
protected boolean afterViewsDone = false;
protected boolean isDebugEnabled = true;
// activity
protected MainActivity activity;
// session
protected Session session;
// manufacturer
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 news
protected String getParentInfos() {
return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
}
// update fragment
protected void update() {
...
// the daughter class is asked to update itself
updateFragment();
}
protected abstract void updateFragment();
}
- 第 7 行:[AbstractFragment] 类继承自 Android 的 [Fragment] 类;
- 每个片段都必须能够更新自身。因此,父类 [AbstractFragment] 要求其子类必须拥有 [updateFragment] 方法(第 68 行),并调用该方法(第 65 行);
- 第 19 行:该类将存储对应用程序 Activity 的引用;
- 第 22 行:该类将存储一个指向会话的引用,其中收集了片段和活动共享的数据;
- 第 25–33 行:抽象类的构造函数;
- 第 27 行:在第 16 行的字段中创建常量 [MainActivity.IS_DEBUG_ENABLED] 的副本;
- 第 28 行:存储实例化类的名称,即子类的名称;
- 第 15–22 行:这些字段带有 [protected] 修饰符,以便子类可以访问它们。请注意,子类并不知道布尔值 [isVisibleToUser] 和 [updateDone](第 10–11 行)的存在;
- 第 57 行:[getParentInfos] 方法带有 [protected] 属性,以便子类可以调用它;
方法 [setUserVisibleHint, onDestroyView, onResume] 与上个项目中的 [PlaceholderFragment] 类保持一致:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// memory
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 {
// we leave the fragment
updateDone = false;
}
}
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
// indicator update
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;
}
}
}
[update] 方法如下:
// update fragment
protected void update() {
// recover activity and session
if (activity == null) {
Activity activity = getActivity();
if (activity != null) {
this.activity = (MainActivity) activity;
this.session = this.activity.getSession();
}
}
// the daughter class is asked to update itself
updateFragment();
}
根据上述代码,当片段的 [update] 方法被调用时,该片段处于可见状态。这一点很重要,因为这意味着 [Fragment.getActivity] 方法随后会返回对应用程序 Activity 的引用(参见第 1.10.8 节),从而可以访问会话。
- 第 4–10 行:如果 Activity 和会话尚未初始化,则进行初始化;
- 第 12 行:调用子类的 [updateFragment] 方法。当该方法执行时,其可访问的 [activity] 和 [session] 字段已初始化;
1.11.4. [PlaceholderFragment] 类
![]() |
[PlaceholderFragment] 类的结构如下:
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 numVisit;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// manufacturer
public PlaceholderFragment() {
super();
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", "constructor");
}
}
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
...
}
// update fragment
public void updateFragment() {
...
}
}
- 第 10 行:[PlaceholderFragment] 类继承自 [AbstractFragment] 类。采用这种架构,编写片段需要:
- 编写 [@AfterViews] 方法,该方法用于在片段的生命周期初始阶段进行初始化,或在先前已发生 [onDestroyView] 事件时重置片段。第 39 行代码对于正确管理片段的生命周期是必需的;
- 编写 [updateFragment] 方法,该方法在片段显示前对其进行更新。此方法可使用其父类的会话;
- 编写片段的事件处理程序。这将在未来的项目中实现;
[@AfterViews] 和 [updateFragment] 方法与上一个项目中的保持一致:
@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;
}
// current text display
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 no
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
// local info for logs
protected String getLocalInfos() {
return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
numVisit, initDone, getActivity() == null);
}
- 第 7 行和第 23 行:在日志中,我们使用继承的方法 [getParentInfos] 显示父类的信息;
1.11.5. [Vue1Fragment] 类
![]() |
[Vue1Fragment] 类的结构与 [PlaceholderFragment] 类相同:
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 {
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// data
private int numVisit;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
}
}
// event manager
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// local info for logs
protected String getLocalInfos() {
return String.format("numVisit=%s", numVisit);
}
// update fragment
@Override
protected void updateFragment() {
// increment visit no
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// the visit number is displayed
Toast.makeText(getActivity(), String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
- 第 9 行:[Vue1Fragment] 类继承自 [AbstractFragment] 类;
- 第 18–26 行:[@AfterViews] 方法没有需要关注的内容。仍需编写该方法,用于将布尔变量 [afterViewsDone] 设置为 true,因为父类会使用此信息;
- 第 42–49 行:[updateFragment] 方法包含显示一条显示访问次数的简短消息(第 48 行)以及在会话中递增该数字(第 44–46 行);
欢迎读者测试这个新项目。
我们将在所有未来的项目中采用这种架构:
- 一个 Activity 和 n 个 Fragment;
- 所有片段都继承自 [AbstractFragment] 类;
- 片段之间以及片段与活动之间共享的数据将放置在 [Session] 类中;
1.11.6. 标签页/片段关联
在负责管理标签页的 [MainActivity] 类中,写有以下内容:
// the tab bar is also associated with the fragment container
// i.e. tab n° i displays fragment n° i of the container
tabLayout.setupWithViewPager(mViewPager);
第 3 行将标签管理器与片段容器关联起来。我们已经看到这种关联的一个结果:当用户点击第 i 个标签时,片段容器会显示第 i 个片段。但我们尚未看到反向情况:当我们要求片段容器显示第 i 个片段时,第 i 个标签会自动被选中。
为演示这一行为,我们将向当前菜单添加 [片段 1、片段 2、...] 等选项。当用户点击 [片段 i] 选项时,我们将要求片段容器显示片段 #i。随后我们将观察标签 #i 是否已被选中。
本步骤首先需要修改应用程序菜单:
![]() | ![]() |
文件 [res/menu/menu_main.xml] 的内容将按以下方式更改:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment5"
android:title="@string/fragment5"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- 第 9–28 行:五个新的菜单选项;
- 选项标签(第 10、14、18、22、26 行)在文件 [res/values/strings.xml] [2] 中定义:
<resources>
<string name="app_name">Exemple-10</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- vue 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 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>
显示效果如下:
![]() |
这些菜单选项的点击处理在 [MainActivity] 类中完成:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// processing 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);
}
}
- 第 2 行:当菜单选项之一被点击时,会调用 [onOptionsItemSelected] 方法;
- 第 8 行:获取被点击选项的 ID;
- 第 9–36 行:通过 switch 语句处理不同情况;
- 第 16–36 行:点击 [Fragment i] 选项会调用第 41–45 行中的 [showFragment(i-1)] 方法;
- 第 43 行:请求片段容器显示所请求的片段;
- 第 42 行:我们首先验证此操作是否可行(条件 1)且是否必要(条件 2);
欢迎读者测试此新版本。我们观察到,当请求显示片段 #i 时,该片段确实被显示出来,且标签 #i 本身也被选中。
既然我们已经了解了标签页与片段的关联机制,接下来将探讨另一种情况:即标签页管理与片段管理解耦的场景。例如,当标签页数量少于片段数量时便是如此。为了演示这一新用例,我们将构建一个新项目。
1.12. 示例-11:标签页与片段分离
1.12.1. 创建项目
我们将 [示例-10] 项目复制为 [示例-11]:
![]() | ![]() |
1.12.2. 目标
新应用程序将包含两个选项卡:
- 第一个选项卡将始终显示片段 [View1];
- 第二个选项卡将显示从菜单中选定的片段;

- 在 [1] 中,显示 [View1] 片段;
- 在 [2] 中,显示用户选定的 [PlaceholderFragment] 片段;
- 在 [3] 中,继续统计访问次数;
1.12.3. 会话
![]() |
新一届会议安排如下:
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// number of fragments visited
private int numVisit;
// n° fragment type [PlaceholderFragment] displayed in second tab
private int numFragment;
// getters and setters
...
}
- 第 10 行:我们将自行处理标签页的点击事件。当点击某个标签页时,我们需要加载上次选中该标签页时显示的片段。字段 [numFragment] 将存储标签页 #2 的片段编号,该编号位于 [0, Fragments_COUNT-2] 范围内。当点击标签页 #2 时,我们将从会话中检索要显示的片段编号;
1.12.4. 菜单
![]() |
菜单 [res / menu / menu_main.xml] 的更改如下:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
标签页 #2 将显示第 9 行至第 24 行中的四个片段之一。第五个片段是 [Vue1Fragment] 片段,它将始终显示在标签页 #1 中。
1.12.5. [MainActivity] 类
[MainActivity] 类现在必须管理标签页及其间的导航,而此前它并未执行此功能。其代码更改如下:
// 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);
// view1 display
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
// at the start there is only one tab
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// event manager
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) {
}
});
...
}
- 第 17 行:片段容器显示的第一个片段将是 [Vue1Fragment] 片段。按设计,这将是容器中的最后一个片段;
- 第 20–22 行:由于我们尚未建立标签页与片段容器之间的关联,因此必须自行管理标签页。最初,第 3 行中的标签栏 [tabLayout] 没有标签页;
- 第 20 行:我们创建第一个标签;
- 第 21 行:为其设置标题。在之前的示例中,标签页标题与片段标题相同。现在情况已不同。因此,我们从片段管理器中移除了 [getPageTitle] 方法。我们不再需要它:
// optional - gives a title to managed fragments
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
- 第 22 行:创建的标签页被添加到标签栏中。我们的标签栏现在有一个标签页。 这个标签页显示什么内容?需要明确的是,标签页和片段是两个独立的概念。显示的片段始终是由片段容器选定的那个。如果你切换标签页却未要求容器更改显示的片段,则不会发生任何变化:显示的仍是同一个片段,但选中的标签页已发生改变。因此,此处显示的片段即第 17 行选定的片段:[Vue1Fragment];
- 第 26–30 行:用于处理用户标签切换的处理方法;
第 26–30 行中的 [onTabSelected] 方法会在每次标签页切换时触发(如果用户点击了已选中的标签页,则不会发生任何变化)。其代码如下:
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (IS_DEBUG_ENABLED) {
Log.d("onglets", "onTabSelected");
}
// a tab has been selected - change the fragment displayed by the fragment container
// miter position
int position = tab.getPosition();
// fragment number to display
int numFragment;
switch (position) {
case 0:
// fragment no. [Vue1Fragment]
numFragment = FRAGMENTS_COUNT - 1;
break;
default:
// fragment no. [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// fragment display
mViewPager.setCurrentItem(numFragment);
}
- 第 8 行:我们获取被点击的标签页的位置。这里,我们将得到数字 0 或 1;
- 第 12–15 行:如果点击了第一个标签页,则准备显示片段 [Vue1Fragment];
- 第 16–18 行:在其他情况下(点击了第 2 个标签页),我们准备重新显示上次选择第 2 个标签页时显示的片段。其 ID 当时已存储在应用的会话中;
- 第 21 行:我们要求片段容器显示目标片段;
现在让我们看看如何管理菜单选项(仍在 [MainActivity] 中):
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// processing 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;
}
- 第 16–31 行:处理 4 个菜单选项。每个处理程序都会调用 [showFragment] 方法,并传入要显示的片段编号;
[showFragment] 方法如下:
// tab n° 2
private TabLayout.Tab tab2 = null;
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// if the 2nd tab doesn't yet exist, we create it
if (tab2 == null) {
tab2 = tabLayout.newTab();
tabLayout.addTab(tab2);
}
// set the title of the second tab
tab2.setText(String.format("Fragment n° %s", (i + 1)));
// change the displayed fragment
mViewPager.setCurrentItem(i);
// the fragment number displayed is set to session
session.setNumFragment(i);
// tab 2 is selected - does nothing if already selected
tab2.select();
}
}
- 请记住,应用程序启动时只有一个标签页;
- 第 2 行:对第 2 个标签页的引用,初始值为 null;
- 第 5 行:显示条件与上一版本相比没有变化;
- 第7–10行:如果标签页 #2 尚未存在,则创建它(第8行)并将其添加到标签栏(第9行);
- 第 12 行:将要显示的片段编号放入第二个标签页的标题中,编号从 1 开始;
- 第 14 行:显示目标片段;
- 第 16 行:将其编号存储在会话中;
- 第 18 行:选中第 2 个标签页。若该标签页已被选中,则不会发生任何操作:[onTabSelected] 方法不会被执行。若该标签页尚未被选中,则会触发 [onTabSelected] 方法。该方法随后会指示片段容器显示第 14 行中已显示的片段。在 [onTabSelected] 方法中进行一个简单的检查即可防止这种情况发生:
// display fragment only if necessary
if (numFragment != mViewPager.getCurrentItem()) {
mViewPager.setCurrentItem(numFragment);
}
欢迎读者测试此新版本。
1.12.6. 改进
现在,我们已经对片段、其生命周期、片段邻接性概念以及它们与标签栏的关系有了扎实的理解。我们还构建了一个稳健的架构,该架构刚刚通过了示例 11 中的测试:
- 一个 Activity 和 n 个片段;
- 所有片段都继承自 [AbstractFragment] 类;
- 片段之间以及片段与 Activity 之间共享的数据存放在 [Session] 类中;
在新的项目中,我们将通过添加一个接口来明确活动与片段之间的关系。
1.13. 示例 12:定义 Activity 与片段之间的关系
在此示例中,我们希望定义 Activity 与片段之间的基本关系。为此,我们将使用:
- 一个接口 [IMainActivity],用于定义片段可以向活动请求的内容;
- 一个抽象类 [AbstractFragment],用于定义每个片段应具备的状态和方法;
1.13.1. 创建项目
我们按照第 1.4 节中的步骤,将 [Example-11] 项目复制为 [Example-12]。最终结果如下:
![]() | ![]() |
1.13.2. [IMainActivity] 接口
从前面的示例可以清楚地看出,片段需要访问由活动实例化的会话。此外,尽管在这些示例中未体现,但可以预见,片段中的事件处理程序有时会导致视图发生变化。届时将要求活动执行此更改。因此,[IMainActivity] 接口可能如下所示:
![]() |
package exemples.android;
public interface IMainActivity {
// session access
Session getSession();
// change of view
void navigateToView(int position);
// debug mode
boolean IS_DEBUG_ENABLED = true;
}
第 12 行:请注意,这里有一个之前位于 [MainActivity] 类中的常量。 我们希望降低片段与活动之间的耦合度,将其限制在 [AbstractFragment] 与 [IMainActivity] 之间的耦合。这样,活动就可以命名为 [MainActivity] 以外的其他名称。由于常量 [IS_DEBUG_ENABLED] 在片段中被使用,因此将其移至 [IMainActivity] 接口中。
1.13.3. 抽象类 [AbstractFragment]
抽象类 [AbstractFragment] 的改动非常小:
// data accessible to daughter 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() {
// recover activity and session
if (mainActivity == null) {
this.activity = getActivity();
if (this.activity != null) {
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
}
}
// the daughter class is asked to update itself
updateFragment();
}
- 第 6 行和第 7 行:我们维护了两种指向该 Activity 的引用:
- 第 6 行:指向实现 [IMainActivity] 接口的 Activity 的引用;
- 第 7 行:指向继承自 Android [Activity] 类的 Activity 的引用。所有 Activity 都是如此;
这两个引用自然指向同一个对象。然而,该对象被视为两种不同的类型。这将阻止运行时进行类型转换;
- 第 14 行:我们使用 [getActivity] 方法获取对该活动的引用;
- 第 15 行:如果该引用不为空,则可以访问会话;
- 第 16–17 行:我们将该 Activity 作为 [IMainActivity] 接口的实现以及会话进行存储;
1.13.4. 修改片段管理器
在 [MainActivity] 类中,对片段适配器 [SectionsPagerAdapter] 进行了一处修改:它不再管理 [Fragment] 类型的片段,而是管理 [AbstractFragment] 类型的片段:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private AbstractFragment[] fragments;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
// initialization of fragment table
fragments = new AbstractFragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
...
}
// a fragment of +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
// fragment n° position
@Override
public AbstractFragment getItem(int position) {
...
}
// makes the number of fragments managed
@Override
public int getCount() {
...
}
}
1.13.5. 修改 [MainActivity] 类
[MainActivity] 类必须实现 [IMainActivity] 接口:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{
...
// injection session
@Bean(Session.class)
protected Session session;
...
// getter session
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// the position view is displayed
if(mViewPager.getCurrentItem()!=position){
// fragment display
mViewPager.setCurrentItem(position);
}
}
- 第 10–12 行:[getSession] 方法已存在;
- 第 15–22 行:[navigateToView] 方法显示片段 #[position];
- 第 17 行:我们检查是否有待处理事项;
- 第 19 行:显示片段 #[position];
此时,运行应用程序。它应该可以正常工作。
1.13.6. 修改 [MainActivity] 中片段的显示方式
目前,[MainActivity] 类使用以下语句显示片段:
// view1 display
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
由于 [navigateToView] 方法的作用相同,请将此类语句(共 2 处)全部替换为:
然后运行应用程序。它应该仍然可以正常工作。
1.13.7. 结论
从现在起,我们将始终使用之前的架构:
- 一个实现 [IMainActivity] 接口的 Activity;
- 继承 [AbstractFragment] 类的片段,这要求它们实现 [updateFragment] 方法。这些片段还必须包含一个 [@AfterViews] 方法,在其中将布尔值 [afterViewsDone] 设置为 true;
- 一个会话,用于封装片段与活动之间共享的数据;
1.14. 示例-13:带片段的示例-05
在 [示例-05] 项目中,我们介绍了视图之间的导航。当时,这涉及活动之间的导航:1 个视图 = 1 个活动。在此,我们提出使用一个活动,并包含多个类型为 [AbstractFragment] 的视图。
1.14.1. 创建项目
我们按照第 1.4 节中的步骤,将之前的 [示例-12] 项目复制为 [示例-13]。最终结果如下:
![]() | ![]() |
1.14.2. 项目结构
我们将开始使用包来组织代码。目前,我们可以区分两个不同的领域:
- 活动管理;
- 片段管理;
为此,我们创建了两个包:[examples.android.activity] 和 [examples.android.fragments]:
![]() |
![]() | ![]() |
我们采用相同的方法创建 [examples.android.fragments] 包:
![]() | ![]() |
在[8]中,我们创建了一个名为[architecture]的第三个包,并将实体[IMainActivity、AbstractFragment、Session、MyPager]放置其中,这些是应用架构的基石。这提醒我们已做出了特定的架构选择。接下来,按照[9]所示移动现有项目元素。每次移动后,都必须点击[重构]按钮进行确认。
此时,编译应用程序。我们在 [MainActivity] 中遇到以下错误:
![]() |
在将类移动到包中时,Android Studio 对应用程序代码进行了必要的修改(例如第 18–21 行)。第 15 行和第 17 行中引用的类并未被移动。它们是由 Android Annotations 库生成的。对于这些类,您必须手动修改导入语句。因此,这些行应修改为:
![]() |
完成上述操作后,编译错误将不复存在。运行应用程序。随后您将看到以下错误:
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{exemples.android/exemples.android.MainActivity_}:
此错误源于应用程序清单:
![]() |
<?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_"
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>
第 3 行和第 12 行指定了指定的 Activity 为 [examples.android.MainActivity_]。但是,由于该 Activity 已移至 [activity] 包,因此第 12 行现在必须改为:
android:name=".activity.MainActivity_"
请注意 [activity] 前的 .。Android Studio 再次无法更新清单文件,因为它引用了一个尚未移动的 Android Annotations 类。因此,使用 AA 库会带来一些不便。
1.14.3. 清理项目
在新项目中:
- 不再有任何标签页、悬浮按钮或菜单;
- 片段 [PlaceholderFragment] 已消失。应用将管理两个片段:我们已有的 [Vue1Fragment],以及我们需要创建的 [Vue2Fragment];
- 会话机制已发生改变;
1.14.3.1. 清理片段
删除 [PlaceHolderFragment] 类 [1]:
![]() | ![]() |
同样,删除与该片段关联的视图 [res/layout/fragment_main.xml] [2]。
1.14.3.2. 清理会话
当前会话内容如下:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// number of fragments visited
private int numVisit;
// n° fragment type [PlaceholderFragment] displayed in second tab
private int numFragment;
// 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;
}
}
我们不会保存本次会话中的任何内容。
编译项目。引发错误的代码行是那些使用了会话内容的行。删除它们。在 [Vue1Fragment] 类中,我们还从代码中删除了 [numVisit] 变量,代码变为如下所示:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.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.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// event manager
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// update fragment
@Override
protected void updateFragment() {
}
}
1.14.3.3. 移除标签页、悬浮按钮和菜单
移除标签页和悬浮按钮的操作在两个地方进行:
- 在视图文件 [res/layout/activity-main.xml] 中,该文件定义了这些元素及其在视图中的位置;
- 在 [MainActivity] 活动代码中;
菜单的移除同样涉及两个位置:
- 在 [res/menu/menu-main.xml] 视图中,该视图定义了菜单选项;
- 在 [MainActivity] 活动代码中;
[res/layout/activity-main.xml] 视图的代码目前如下:
<?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>
<exemples.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>
- 删除第 [28-31, 41-47] 行;
- 同时删除第 18-24 行中的工具栏;
菜单代码 [res / menu / menu_main.xml] 目前如下:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".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>
- 我们将删除第 9 至 24 行。这会留下一个我们不会使用的选项。此处仅为提供一个可通过复制/粘贴复制的菜单选项声明示例;
在 [MainActivity] 类中,删除所有涉及标签页、悬浮按钮、工具栏和菜单的内容。查找这些引用最简单的方法是删除它们的声明:
// the tab manager
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// the floating button
@ViewById(R.id.fab)
protected FloatingActionButton fab;
然后重新编译应用。出现错误的行是引用了缺失元素的那些。因此请删除所有这些行。此外,修改片段管理器,使其不再引用我们已删除的 [PlaceholderFragment] 片段:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private AbstractFragment[] fragments;
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
}
// fragment n° position
@Override
public AbstractFragment getItem(int position) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// makes the number of fragments managed
@Override
public int getCount() {
return fragments.length;
}
}
- 第 7–10 行:我们已移除了所有片段生成代码;
此时,应该不再会有任何编译错误。在 [MainActivity] 类中,我们得到了以下中间代码:
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 exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.IMainActivity;
import exemples.android.architecture.MyPager;
import exemples.android.architecture.Session;
import exemples.android.fragments.Vue1Fragment_;
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;
// injection session
@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;
// manufacturer
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 application name is displayed
setSupportActionBar(toolbar);
// the fragment manager
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e. fragment no. i in the fragment container is fragment no. i issued by the fragment manager
mViewPager.setAdapter(mSectionsPagerAdapter);
// fragment offset
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// inhibit swiping between fragments
mViewPager.setSwipeEnabled(false);
// no scrolling
mViewPager.setScrollingEnabled(false);
// view1 display
navigateToView(FRAGMENTS_COUNT - 1);
}
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
}
// getter session
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// the position view is displayed
if (mViewPager.getCurrentItem() != position) {
// fragment display
mViewPager.setCurrentItem(position);
}
}
// the fragment manager
// it is used to request fragments to be displayed in the main view
// must define methods [getItem] and [getCount] - the others are optional
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private AbstractFragment[] fragments;
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
}
// fragment n° position
@Override
public AbstractFragment getItem(int position) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// makes the number of fragments managed
@Override
public int getCount() {
return fragments.length;
}
}
}
还需要做一些修改:
- 删除第 31 行,该行已不再需要;
- 第33行:将片段邻接度设为1;
- 第 76 行:导航至视图 0。这将是第一个显示的视图;
- 第 108 行:使用片段 [Vue1Fragment_] 初始化数组:
// fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
因此我们只有一个片段。运行应用程序。您应该会得到以下结果:

[Validate] 按钮应该可以正常工作。
1.14.4. 创建片段及相关视图
该应用程序将包含两个视图,它们来自 [Example-05] 项目。当前项目中已有 [vue1.xml] 视图。现在我们将 [Example-05] 中的 [vue2.xml] 复制到 [Example-12] 中(打开这两个项目,并在它们之间进行复制粘贴)。
![]() | ![]() |
- 在 [1] 中,是新的视图。当我们尝试编辑它时,会出现错误 [2]。我们需要修改 [strings.xml] 文件 [3],以添加此新视图引用的字符串:
<resources>
<string name="app_name">Exemple-13</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- vue 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<!-- vue 2 -->
<string name="btn_vue2">Vue n° 2</string>
<string name="titre_vue2">Vue n° 2</string>
<string name="btn_vue1">Vue n° 1</string>
</resources>
我们将 [View1Fragment] 类复制为 [View2Fragment]:
![]() |
并将复制的代码修改如下:
package exemples.android.fragments;
import android.util.Log;
import exemples.android.R;
import exemples.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("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// update fragment
@Override
protected void updateFragment() {
}
}
- 第 9 行:该片段与视图 [res/layout/view2.xml] 相关联;
- 第 10 行:该类继承自抽象类 [AbstractFragment];
- 第 12–20 行:必需的 [@AfterViews] 方法;
- 第 23–25 行:必需的 [updateFragment] 方法;
1.14.5. 实现片段及其间的导航
该 Activity 现在将管理两个片段。其 [SectionsPagerAdapter] 类更新如下:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
}
[IMainActivity] 接口通过其 [navigateToView] 方法处理视图之间的导航。我们将处理 [Vue1Fragment] 片段中 [View 2] 按钮的点击事件:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.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.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// event managers ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
mainActivity.navigateToView(1);
}
// update fragment
@Override
protected void updateFragment() {
}
}
- 第 37–40 行:[showVue2] 方法处理 [View #2] 按钮上的 'click' 事件;
- 第 39 行:使用 Activity 的 [navigateToView] 方法进行导航。回顾一下,该 Activity 在父类中是这样存储的:
// activity
protected IMainActivity mainActivity;
并且在进入任何事件处理程序时,该活动已经初始化完毕。
- 第 34 行:该语句使用了父类的 [activity] 变量,该变量是对该 Activity 作为 Android [Activity] 类型的实例的引用;
protected Activity activity;
我们在 [Vue2Fragment] 片段中也发现了类似的代码:
package exemples.android.fragments;
import android.util.Log;
import exemples.android.R;
import exemples.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("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gestionnaires d'évts ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// update fragment
@Override
protected void updateFragment() {
}
}
- 第 24–27 行:[showVue1] 方法处理 [View 1] 按钮的“点击”事件;
运行项目并验证视图间的导航是否正常。
1.14.6. 定义会话
应用程序的工作原理如下:
- 在视图 1 中输入一个名称;
- 在视图 2 中显示该名称;
为了让视图 1 将输入的名称传递给视图 2,我们将使用以下会话;
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// name
private String nom;
// getters and setters
...
}
- 第 8 行:输入的名称;
[MainActivity] 类将按以下方式初始化会话:
// injection session
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// init session
session.setNom("");
}
1.14.7. 片段的最终代码
在 [Vue1Fragment] 片段中,我们修改了 [Validate] 按钮的点击处理程序代码:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.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.editTextNom)
protected EditText editTextNom;
...
// event managers ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// memorizes the name entered
String nom = editTextNom.getText().toString();
// we display it
Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
// enter the name entered in the session
session.setNom(editTextNom.getText().toString());
// navigate to view no. 2
mainActivity.navigateToView(1);
}
// update fragment
@Override
protected void updateFragment() {
}
}
- 第 31-37 行:处理 [View #2] 按钮的点击事件;
- 第 34 行:在跳转到视图 2 之前,我们将输入的名称存储在会话中,以便新视图可以访问它;
[View2Fragment] 视图的演变如下:
package exemples.android.fragments;
import android.util.Log;
import android.widget.TextView;
import exemples.android.R;
import exemples.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 {
// visual interface components
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gestionnaires d'évts ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// update fragment
@Override
protected void updateFragment() {
// retrieve the name entered in the session
String nom = session.getNom();
// we display it
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
当显示视图 #2 时,必须显示在视图 #1 中输入的名称。我们知道,该视图显示后,其 [updateFragment] 方法会立即被调用。因此,我们可以在该方法(第 36–42 行)中放置用于显示名称的代码。
- 第16–17行:声明视图的唯一视觉组件;
- 第 39 行:从会话中检索视图 1 中输入的名称;
- 第 41 行:更新标签 [textViewBonjour];
运行项目并验证其是否正常工作。
1.14.8. 管理片段的生命周期
在 [Vue1Fragment] 片段中,[@AfterViews] 方法如下:
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
此方法尚不完善。实际上,我们必须始终考虑片段在 [onDestroyView] 操作后被回收的情况。在此情况下,片段 1 的视图会被重新生成,而之前可能输入的任何名称都会从视图中消失。 我们不希望出现这种情况。目前,输入的名称仍会显示,因为 Fragment 1 的片段是相邻的,这意味着 [Vue1Fragment] 片段的生命周期仅执行一次。然而,最好还是考虑片段被回收的情况。
有几种方法可以解决这个问题:
- 我们可以利用 [update] 方法在每次显示片段时都会被系统性执行这一特性,来更新已输入的名称;
- 也可以仅在 [@AfterViews] 方法重新执行时进行此更新。我们将采用后一种方法;
我们将 [View1Fragment] 中的代码修改如下:
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// data
private String nom;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// the text displayed is (re)initialized
editTextNom.setText(nom);
}
// event managers ----------------------------------
...
@Click(R.id.buttonVue2)
protected void showVue2() {
// the name entered is noted so that it can be retrieved if the fragment is recycled
nom = editTextNom.getText().toString();
// enter the name entered in the session
session.setNom(nom);
// navigate to view no. 2
activity.navigateToView(1);
}
- 第 27 行:在即将从视图 1 跳转到视图 2 时,我们保存已输入的姓名;
- 第 17 行:每次片段的生命周期被执行时,都会再次显示上次输入的姓名;
对于 [View2Fragment] 片段,现有代码已足够:
// visual interface components
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// update fragment
@Override
protected void updateFragment() {
// retrieve the name entered in the session
String nom = session.getNom();
// we display it
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
- 视图中唯一的视觉组件(第 3 行)会在每次显示视图时(第 21 行)进行更新。因此,[@AfterViews] 方法无需额外处理;
1.14.9. 结论
至此,我们再次验证了该架构的合理性:
- 一个实现 [IMainActivity] 接口的活动;
- 继承 [AbstractFragment] 类的片段,该类要求它们实现 [updateFragment] 方法。这些片段还必须拥有一个 [@AfterViews] 方法,其中将布尔值 [afterViewsDone] 设置为 true;
- 一个会话,用于封装片段与活动之间共享的数据;
1.15. 示例-14:两层架构
我们将构建一个具有以下架构的单视图应用程序:
![]() |
1.15.1. 创建项目
我们按照第1.4节中的步骤,将之前的项目[Example-12]复制为[Example-13]。结果如下:
![]() | ![]() |
1.15.2. 视图 [view1]
该应用程序将仅包含一个视图 [view1.xml]。因此,我们将删除另一个视图 [view2.xml] 及其关联的片段:
![]() | ![]() |
编译应用程序。在 [MainActivity] 中出现错误:
![]() |
请在片段管理器中修正以下第 4 行 [SectionsPagerAdapter]
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
上面的第 4 行变为:
// fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
删除不再需要的导入语句 [Ctrl-Shift-O]。此时不应再出现任何编译错误。运行项目:视图 #1 应该会出现。现在我们将对其进行修改。
我们将创建一个视图 [vue1.xml] 来生成随机数:
![]() |
其组件如下:
其 XML 代码如下:
<?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_Titre2"
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_errorIntervalle"
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_errorIntervalle"
android:textColor="@color/red" />
<Button
android:id="@+id/btn_Executer"
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_executer" />
<TextView
android:id="@+id/txt_Reponses"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btn_Executer"
android:layout_marginTop="30dp"
android:text="@string/list_reponses"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/blue" />
<ListView
android:id="@+id/lst_reponses"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_Reponses"
android:layout_marginTop="40dp"
android:background="@color/wheat"
android:clickable="true"
tools:listitem="@android:layout/simple_list_item_1" >
</ListView>
</RelativeLayout>
上一个视图使用了在 [res/values/strings.xml] 文件中定义的标签:
<resources>
<string name="app_name">Exemple-14</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- vue 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="list_reponses">Liste des réponses</string>
<string name="btn_executer">Exécuter</string>
<string name="aleas">Génération de N nombres aléatoires</string>
<string name="txt_nbaleas">Valeur de N :</string>
<string name="txt_a">"Intervalle [a,b] de génération, a : "</string>
<string name="txt_b">"b : "</string>
<string name="txt_dummy">Dummy</string>
<string name="txt_errorNbAleas">Tapez un nombre entier >=1</string>
<string name="txt_errorIntervalle">Les bornes de l\'intervalle doivent être entières et b>=a</string>
</resources>
[vue1.xml] 中使用的颜色在文件 [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>
<!-- colors applied -->
<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. 会话
![]() |
由于这里只有一个片段,因此无需规划片段间的通信。因此,该会话将为空:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
}
此时,编译应用程序。在使用了现已为空的会话中元素的行上会出现错误。删除这些行,并验证编译是否不再报错。
1.15.4. [Vue1Fragment] 片段
![]() |
我们将现有的 [Vue1Fragment] 片段修改如下:
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 exemples.android.R;
import exemples.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 listReponses;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNbAleas;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
protected TextView txtErrorIntervalle;
// list of order responses
private List<String> reponses = new ArrayList<>();
// listview adapter
private ArrayAdapter<String> adapterReponses;
// seizures
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// hide error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
}
@Click(R.id.btn_Executer)
void doExecuter() {
// hide any previous error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// test the validity of entries
if (!isPageValid()) {
return;
}
}
// check the validity of the data entered
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
}
}
- 这里只有一个片段,其生命周期仅在应用程序启动时执行一次。因此,[@AfterViews] 方法(第 46–57 行)和 [updateFragment] 方法(第 75–81 行)仅在应用程序启动时执行一次;
- 第 55–56 行:我们将视图中的两条错误消息隐藏起来(如下所示)[1-2];
![]() |
- 第 59-60 行:点击 [Execute] 按钮时执行的方法;
- 第71-73行:检查输入内容的有效性;
[isPageValid] 方法如下:
// seizures
private int nbAleas;
private int a;
private int b;
...
// check the validity of the data entered
private boolean isPageValid() {
// enter the number of random numbers
nbAleas = 0;
Boolean erreur;
int nbErreurs = 0;
try {
nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
erreur = (nbAleas < 1);
} catch (Exception ex) {
erreur = true;
}
// mistake?
if (erreur) {
nbErreurs++;
txtErrorAleas.setVisibility(View.VISIBLE);
}
// enter a
a = 0;
erreur = false;
try {
a = Integer.parseInt(edtA.getText().toString());
} catch (Exception ex) {
erreur = true;
}
// mistake?
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// b input
b = 0;
erreur = false;
try {
b = Integer.parseInt(edtB.getText().toString());
erreur = b < a;
} catch (Exception ex) {
erreur = true;
}
// mistake?
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// return
return (nbErreurs == 0);
}
- 第 2–4 行:这三个字段由 [isPageValid] 方法初始化。此外,如果所有条目均有效,该方法返回 true;否则返回 false。如果存在任何无效条目,将显示相应的错误信息;
此时,应用程序已可运行。请输入错误数据以验证 [isPageValid] 方法的功能。
1.15.5. [业务]层
![]() |
![]() |
[业务]层提供了以下[IMetier]接口:
package exemples.android.metier;
import java.util.List;
public interface IMetier {
List<Object> getAleas(int a, int b, int n);
}
方法 [getAleas(a,b,n)] 通常返回 n 个落在 [a,b] 范围内的随机整数。该方法还设计为每三次调用中会抛出一次异常,且该异常会被包含在方法返回的结果中。最终,该方法返回一个由 [Exception] 或 [Integer] 类型对象组成的列表。
该接口的 [Metier] 实现如下:
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 Metier implements IMetier {
public List<Object> getAleas(int a, int b, int n) {
// object list
List<Object> réponses = new ArrayList<Object>();
// some checks
if (n < 1) {
réponses.add(new AleaException("Le nombre d'entier aléatoires demandé doit être supérieur ou égal à 1"));
}
if (a < 0) {
réponses.add(new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0"));
}
if (b < 0) {
réponses.add(new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0"));
}
if (a >= b) {
réponses.add(new AleaException("Dans l'intervalle [a,b], on doit avoir a< b"));
}
// mistake?
if (réponses.size() != 0) {
return réponses;
}
// random numbers are generated
Random random = new Random();
for (int i = 0; i < n; i++) {
// generate a random exception 1 time / 3
int nombre = random.nextInt(3);
if (nombre == 0) {
réponses.add(new AleaException("Exception aléatoire"));
} else {
// otherwise a random number is returned between two bounds [a,b]
réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
}
}
// result
return réponses;
}
}
- 第 9 行:我们在 [Business] 类上使用 AA 注解 [@EBean],以便将其引用注入到 [Presentation] 层。该属性(scope = EBean.Scope.Singleton)确保 [Business] 类仅会创建一个实例。因此,如果该引用被多次注入到 [Presentation] 层,注入的始终是同一个引用;
- 其余代码均为标准写法;
[Metier] 类使用的 [AleaException] 类型如下:
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);
}
}
- 第 3 行:[AleaException] 类继承了系统类 [RuntimeException],因此它是一个未处理的异常:无需在 try/catch 代码块中进行处理,也不需要包含在方法签名中;
1.15.6. 重新审视 [MainActivity]
![]() |
[业务] 层 活动 用户 视图
该 Activity 将实现 [business] 层的 [IMetier] 接口。因此,片段/视图仅与该 Activity 相关联。
[MainActivity] 活动已实现了 [IMainActivity] 接口。若要使其同时实现 [IMetier] 接口,我们可以:
- 将 [IMetier] 接口添加到该 Activity 已实现的接口列表中;
- 确保 [IMainActivity] 接口本身继承自 [IMetier] 接口。这是我们采用的方法;
[IMainActivity] 接口将变为如下形式:
![]() |
package exemples.android.architecture;
import exemples.android.metier.IMetier;
public interface IMainActivity extends IMetier {
// session access
Session getSession();
// change of view
void navigateToView(int position);
// debug mode
public static final boolean IS_DEBUG_ENABLED = true;
}
- 第 5 行:[IMainActivity] 接口继承自 [IMetier] 接口
[MainActivity] 类的演变如下:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
...
// injection session
@Bean(Session.class)
protected Session session;
// injection molding
@Bean(Metier.class)
protected IMetier metier;
...
// implémentation IMetier --------------------------------------------------------------------
@Override
public List<Object> getAleas(int a, int b, int n) {
return metier.getAleas(a, b, n);
}
- 第 11-12 行:将 [business] 层注入到 Activity 中。为此,我们使用 [@Bean] 注解,其参数是带有 [@EBean] 注解的类;
- 第 2 行:该 Activity 实现了 [IMainActivity] 接口,因此也实现了 [业务] 层的 [IMetier] 接口;
- 第 16–19 行:实现 [IMetier] 接口的唯一方法。我们仅将调用委托给 [business] 层;
1.15.7. 重新审视 [Vue1Fragment] 片段
![]() |
[Vue1Fragment] 类的代码演变如下:
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 exemples.android.R;
import exemples.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 listReponses;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNbAleas;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
protected TextView txtErrorIntervalle;
// list of order responses
private List<String> reponses = new ArrayList<>();
// listview adapter
private ArrayAdapter<String> adapterReponses;
// seizures
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
...
}
@Click(R.id.btn_Executer)
void doExecuter() {
...
}
// check the validity of the data entered
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
// will only be executed once when the application is started
// create the ListView adapter - this requires the [activity] variable to have been initialized
adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
}
- 第 69-70 行:为 [ListView] 组件设置适配器;
[ListView] 组件用于显示项目列表。它通过 [ListAdapter] 适配器实现此功能,该适配器本身连接到为 [ListView] 提供数据的源。要为 [ListView] 定义适配器,请使用以下 [ListView.setAdapter] 方法:
public void setAdapter (ListAdapter adapter)
[ListAdapter] 是一个接口。[ArrayAdapter] 类是实现该接口的类。上文第 69 行中使用的构造函数如下:
- [context] 是显示 [ListView] 的 Activity;
- [resource] 是用于标识 [ListView] 中某项所用视图的整数。该视图可以具有任意复杂度,开发者可根据需求自行构建;
- [textViewResourceId] 是用于标识 [resource] 视图中某个 [TextView] 组件的整数。显示的字符串将通过该组件呈现;
- [objects]:[ListView] 显示的对象列表。将使用这些对象的 [toString] 方法,在由 [resource] 标识的视图中,通过由 [textViewResourceId] 标识的 [TextView] 显示该对象。
开发者的任务是创建用于显示 [ListView] 中每个项的 [resource] 视图。对于像这里这样仅需显示单个字符串的简单情况,Android 提供了由 [android.R.layout.simple_list_item_1] 标识的视图。 该视图包含一个标识符为 [android.R.id.text1] 的 [TextView] 组件。第 69 行调用的正是用于创建 [ListView] 适配器的方法。该适配器只需定义一次。为了便于复用,它已被定义为类的实例变量(第 39 行)。让我们再次查看第 69 行:
adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
[ArrayAdapter] 构造函数的第一个参数是通过 [getActivity] 从片段获取的 Activity,并存储在父类的 [activity] 变量中。该字段并非总是具有值。因此,日志显示当我们到达 [@AfterViews] 方法时,它尚未初始化,所以我们不能将第 69–70 行放在该方法中。 而在 [updateFragment] 方法中,这是可行的,因为我们知道当该方法被调用时,[activity] 必然不为空。此处的适配器关联了第 37 行定义的 [responses] 数据源;
[doExecute] 方法处理 [Execute] 按钮的点击事件。其代码如下:
@Click(R.id.btn_Executer)
void doExecuter() {
// hide any previous error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// delete previous answers
reponses.clear();
adapterReponses.notifyDataSetChanged();
// test the validity of entries
if (!isPageValid()) {
return;
}
// we ask for the random numbers in the activity
List<Object> data = mainActivity.getAleas(a, b, nbAleas);
// we create a list of Strings from this data
for (Object o : data) {
if (o instanceof Exception) {
reponses.add(((Exception) o).getMessage());
} else {
reponses.add(o.toString());
}
}
// refresh listview
adapterReponses.notifyDataSetChanged();
}
- 第 7-8 行:我们需要清空 ListView。为此,我们清空数据源 [reponses],并要求与 ListView 关联的适配器进行刷新;
- 第 10-12 行:在执行请求的操作之前,我们验证输入的值是否正确;
- 第 14 行:从 Activity 请求随机数列表。我们获得一个对象列表,其中每个对象的类型为 [Integer] 或 [AleaException];
- 第 16–22 行:利用获取到的对象列表,更新 ListView 显示的 [reponses] 数据源;
- 第 24 行:请求 ListView 适配器刷新;
1.15.8. 执行
运行该项目并验证其是否正常工作。
1.16. 示例-15:客户端/服务器架构
接下来,我们将探讨一种常见的 Android 应用架构,即 Android 应用与远程 Web 服务进行通信的架构。我们将采用以下架构:
![]() |
我们在 Android 应用中添加了一个 [DAO] 层,用于与远程服务器通信。该层将与生成随机数并显示在 Android 平板电脑上的服务器进行通信。该服务器将采用以下两层架构:
![]() |
客户端向 [web/JSON] 层查询特定 URL,并接收 JSON(JavaScript 对象表示法)格式的文本响应。在此,我们的 Web 服务将处理形式为 [/a/b] 的单一 URL,该 URL 将返回一个落在 [a,b] 范围内的随机数。我们将按以下顺序描述该应用程序:
服务器
- 其 [业务] 层;
- 其基于 Spring MVC 实现的 [Web/JSON] 服务;
客户端
- 其 [DAO] 层。将不包含 [业务] 层;
1.16.1. [Web/JSON] 服务器
我们希望构建以下架构:
![]() |
1.16.1.1. 项目创建
我们将使用 Spring 生态系统 [http://spring.io/] 构建 Web 服务。访问网站 [http://start.spring.io/](2016 年 6 月),该网站可帮助我们生成包含项目所需依赖项的 Gradle 项目——该项目并非 Android 项目,因此 Android Studio 无法提供支持:
![]() |
- 在 [1] 中:选择 Gradle 项目;
- 在 [2-3]:设置项目生成的 JAR 依赖项属性(见下文);
- 在 [4] 中:选择 Web 依赖项 [5],以便获取 Web 服务所需的二进制文件;
- 在 [6] 中:生成项目。随后将生成一个 Gradle 模板项目的 ZIP 文件并提供下载;
[2-3] 处应填写什么内容?我们之前已使用过 Gradle 依赖项。例如,上一个项目中的依赖项如下:
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
...
}
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'
}
- 第 22 行:依赖项的格式为 [groupId:artifactId:version]。而在 [http://start.spring.io/] 表单中要求的是:
- 在 [2] 中是 [groupId];
- 在 [3] 中是 [artifactId];
将获取的 zip 文件解压到包含其他项目的文件夹中:
![]() | ![]() ![]() | ![]() |
使用 Android Studio 打开 Gradle 项目 [server-01] [1-2]。打开后的项目位于 [3](项目视图)中。
1.16.1.2. Gradle 配置
![]() |
生成的 Gradle 文件(2016 年 6 月)如下:
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'
}
}
- 第 14 行和第 34–38 行是针对 Eclipse IDE 的。我们将它们删除;
- 第 1–11 行和第 15 行用于向我们的 Gradle 项目添加一个名为 [spring-boot] 的插件。 Spring Boot 是 Spring 生态系统中的一个项目 [http://projects.spring.io/spring-boot/]。该插件定义了 Spring 最常用的依赖项版本。这使我们可以省略指定其版本(第 30 和 31 行)。此时,版本即为所用 Spring Boot 版本所定义的版本(第 3 行);
- 第 22–23 行:要使用的 Java 版本,此处为 1.8;
- 第 25–27 行:用于下载依赖项的二进制仓库;
- 第 26 行:指定 Maven Central 仓库。这是目前最大的开源二进制仓库;
- 第 29–32 行:项目所需的依赖项:
- 第 30 行:此依赖项包含构建 Spring Web 服务所需的所有二进制文件;
- 第 31 行:此依赖项包含测试所需的所有二进制文件,特别是 JUnit 测试;
- [compile] 依赖项表示该项目需要该依赖项进行编译。而 [testCompile] 依赖项表示该项目仅在运行测试时才需要该依赖项,因此不会包含在项目二进制文件中;
我们将从清理 Gradle 文件开始:
// 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'
// binaire du projet
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
// versions Java
sourceCompatibility = 1.8
targetCompatibility = 1.8
// dépôts Maven
repositories {
mavenLocal()
mavenCentral()
}
// dépendances
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
- 第 30 行:我们为开发机添加了本地 Maven 仓库。该仓库在安装 Maven 时自动创建(参见第 6.10 节)。如果请求的依赖项已存在于本地 Maven 仓库中,则不会从中央 Maven 仓库获取;
- 第 19–22 行:一个用于生成项目二进制文件的 Gradle 任务。我们将通过它来查看具体执行了哪些操作;
![]() | ![]() | ![]() |
- 在 [1-4] 中,运行 [build.gradle] 文件中定义的 [jar] 任务([1] 位于 IDE 右上角及侧边);
上一步操作会生成项目的 JAR 归档文件,并将其放置在 [build/libs] 文件夹中 [5]:
![]() |
该归档文件的名称直接来源于 [build.gradle] 文件中提供给 [jar] 任务的信息(第 19–22 行)。
该项目的所有依赖项可按以下方式查看:
![]() |
从 [1] 中可以看出,该项目的唯一依赖项 [compile('org.springframework.boot:spring-boot-starter-web')] 带来了数十个二进制文件。Web 版的 Spring Boot 已包含了 Spring MVC Web 应用程序可能需要的依赖项。这意味着其中一些可能是不必要的。Spring Boot 非常适合用于教程:
- 它包含了我们可能需要的依赖项;
- 它包含了一个嵌入式 Tomcat 服务器 [1],这让我们无需将应用程序部署到外部 Web 服务器上;
您可以在 Spring 生态系统网站 [http://spring.io/guides] 上找到许多使用 Spring Boot 的示例。
现在,我们将按以下方式完善 [build.gradle] 文件:
// spring boot
...
// dépendances
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
// plugin pour créer un binaire aux normes Maven dans le dépôt Maven local
apply plugin: 'maven-publish'
publishing {
publications {
maven(MavenPublication) {
groupId 'istia.st.exemples.android'
artifactId 'server-01'
version '0.0.1-SNAPSHOT'
from components.java
}
}
repositories {
maven {
// change to point to your repo, e.g. http://my.org/repo
url 'file://D:\\maven'
}
}
}
- 第 10 行:我们导入了一个名为 [maven-publish] 的 Gradle 插件,它允许我们按照 Maven 标准将项目的二进制文件发布到 Maven 仓库;
- 第 11 行:一个名为 [publishing] 的 Gradle 任务;
- 第 14–15 行:将要生成的 Maven 二进制文件的属性;
- 第 23 行:将发布到的 Maven 仓库,本例中为本地 Maven 仓库;
添加 [maven-publish] 插件已在 Gradle 项目中创建了新的任务:
![]() | ![]() |
如果在 [2] 中运行 [publish] 任务,项目二进制文件将被生成并安装在 [build.gradle] 文件第 23 行指定的文件夹中:
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |
![]() |
[jar] 任务会生成项目的二进制文件。该二进制文件不包含其依赖项,因此无法直接执行。我们可以生成一个包含所有依赖项的可执行二进制文件。为此,我们需要在 [build.gradle] 文件中添加以下代码:
// créer un binaire avec toutes ses dépendances
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 { it.isDirectory() ? it : zipTree(it) } }
with jar
}
- 第 6 行:输入项目可执行类的完整名称:
![]() |
该类的代码如下:
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);
}
}
刷新 Gradle 项目,然后运行 [fatJar] 任务:
![]() | ![]() |
生成的二进制文件位于 [build/libs] 文件夹中,并可运行 [1-7]:
![]() | ![]() |
1.16.1.3. 项目配置
仅靠 Gradle 配置是不够的。我们还需要配置项目。由于这不是由 IDE 生成的 Android 项目,因此必须在此处进行此项配置——这是我们此前尚未完成的步骤。
![]() | ![]() |
- 在[3-4]中:使用 JDK 1.8;
要编译该项目,Android 项目中原本可用的按钮已不复存在。我们将使用菜单选项 [1-2]:
![]() | ![]() |
接下来,系统会提示读者创建以下项目。我们将对最终的项目代码进行说明 [3]。
1.16.1.4. [业务]层
![]() |
![]() |
[业务]层采用与前一个示例中的[业务]层相同的方法。它将具有以下[IMetier]接口:
package exemples.android.server.metier;
public interface IMetier {
// random number in [a,b]
int getAlea(int a, int b);
}
- 第 5 行:生成 1 个 [a,b] 区间内随机数的方法
实现该接口的 [Metier] 类的代码如下:
package exemples.android.server.metier;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Random;
@Service
public class Metier implements IMetier {
@Override
public int getAlea(int a, int b) {
// some checks
if (a < 0) {
throw new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0", 2);
}
if (b < 0) {
throw new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0", 3);
}
if (a >= b) {
throw new AleaException("Dans l'intervalle [a,b], on doit avoir a< b", 4);
}
// result generation
Random random=new Random();
random.setSeed(new Date().getTime());
return a + random.nextInt(b - a + 1);
}
}
我们不再对该类进行说明:它与前一个示例中的类类似,只是不会随机抛出异常。请注意第 8 行中的 Spring 注解 [@Service],它会促使 Spring 将该类实例化为单例(singleton),并使其引用可供其他 Spring 组件使用。此处也可以使用其他 Spring 注解来达到相同的效果。 Spring 组件具有默认名称,该名称可作为所用注解的属性进行指定。如果不指定该属性(如本例所示),Spring 组件将采用类名的首字母小写形式作为名称。因此,在此处,该 Spring 组件默认命名为 [metier];
[Metier] 类会抛出 [AleaException] 类型的异常:
package exemples.android.server.metier;
public class AleaException extends RuntimeException {
// error code
private int code;
// manufacturers
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
....
}
- 第 3 行:[AleaException] 继承自 [RuntimeException] 类。因此,它是一个未处理的异常(无需使用 try/catch 进行处理);
- 第 6 行:向 [RuntimeException] 类添加了一个错误代码;
1.16.1.5. Web 服务 / JSON
![]() |
![]() |
该 Web 服务/JSON 由 Spring MVC 实现。Spring MVC 通过以下方式实现 MVC(模型-视图-控制器)架构模式:
![]() |
客户端请求的处理流程如下:
- 请求 - 请求的 URL 通常采用 http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... 的形式。[Dispatcher Servlet] 是 Spring 框架中负责处理传入 URL 的类。它会将 URL “路由”到应处理该请求的操作(Action)。这些操作是称为 [控制器(Controller)] 的特定类中的方法。 此处的 MVC 中的 C 指代 [Dispatcher Servlet、Controller、Action] 这一链条。如果未配置任何 Action 来处理传入的 URL,[Dispatcher Servlet] 将返回请求的 URL 未找到(404 NOT FOUND 错误);
- 处理
- 被选中的 Action 可以使用 [Dispatcher Servlet] 传递给它的参数。这些参数可能来自多个来源:
- URL 的路径 [/param1/param2/...],
- URL 参数 [p1=v1&p2=v2],
- 浏览器随请求提交的参数;
- 在处理用户请求时,该操作可能需要调用[业务]层[2b]。一旦处理完客户端的请求,可能会触发各种响应。一个典型的例子是:
- 如果请求无法正确处理,则显示错误页面
- 否则则显示确认页面
- 操作会指示显示特定的视图 [3]。该视图将展示被称为视图模型的数据。这就是 MVC 中的 M。操作会创建这个 M 模型 [2c],并指示显示 V 视图 [3];
- 响应——选定的视图 V 使用操作生成的模型 M 来初始化其必须发送给客户端的 HTML 响应中的动态部分,然后发送该响应。
对于 Web 服务/JSON,上述架构稍作修改:
![]() |
- 在 [4a] 中,模型(即一个 Java 类)通过 JSON 库转换为 JSON 字符串;
- 在 [4b] 中,该 JSON 字符串被发送至浏览器;
第 6.14 节的附录中给出了将 Java 对象序列化为 JSON 字符串以及将 JSON 字符串反序列化为 Java 对象的示例。
让我们回到应用程序的 [web] 层:
![]() |
在我们的应用程序中,只有一个控制器:
![]() |
Web/JSON 服务将向其客户端发送如下所示的 [Response] 类型的响应:
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;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
- 第 13 行:[T body] 字段是客户端预期的响应。我们决定在此处使用类型为 T 的通用响应,而非预期随机数的 Integer 类型。我们希望能在其他情况下复用这个类。在处理客户端请求时,服务器可能会遇到问题,这些问题随后会在另外两个字段中进行总结;
- 第 8 行:状态码(无错误时为 0);
- 第 9 行:如果 status != 0,则返回一个错误消息列表——通常是发生异常时来自异常堆栈的消息;如果没有错误,则返回 null;
控制器 [WebController] 如下所示:
package exemples.android.server.web;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.server.metier.AleaException;
import exemples.android.server.metier.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 IMetier metier;
// mapper JSON
@Autowired
private ObjectMapper mapper;
// random numbers
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
// the answer
Response<Integer> response = new Response<>();
// we use the business layer
try {
response.setBody(metier.getAlea(a, b));
response.setStatus(0);
} catch (AleaException e) {
response.setStatus(e.getCode());
response.setMessages(getMessagesFromException(e));
}
// we return the answer
return mapper.writeValueAsString(response);
}
private List<String> getMessagesFromException(Throwable e) {
// message list
List<String> messages = new ArrayList<String>();
// browse the exception stack
Throwable th = e;
while (th != null) {
messages.add(e.getMessage());
th = th.getCause();
}
// we return the result
return messages;
}
}
- 第 17 行:[@Controller] 注解表明该类是一个 MVC 控制器,其方法负责处理 Web 应用程序中特定 URL 的请求;
- 第 21–22 行:[@Autowired] 注解指示 Spring 将类型为 [IMetier] 的组件注入该字段。这将对应前面的 [Metier] 类。由于我们为其添加了 [@Service] 注解,因此它被视为 Spring 组件;
- 第 24–25 行:我们对稍后将定义的 JSON 映射器也进行了同样的操作。我们的 Web 服务将以 JSON 字符串的形式发送响应。该映射器将负责将响应序列化为 JSON;
- 第 30 行:生成随机数的方法。其名称并不重要。当该方法运行时,其参数已由 Spring MVC 初始化。我们稍后将了解具体实现。此外,如果该方法被调用,说明 Web 服务器已收到针对第 28 行 URL 的 HTTP GET 请求;
- 第 28 行:[@RequestMapping] 注解定义了被注解方法的某些属性:
- [value]:该方法接受的 URL;
- [method]:该方法接受的 HTTP 方法。主要有两种:GET 和 POST。[POST] 方法用于客户端希望在其 HTTP 请求中附加文档时;
- [produces]:设置将发送给客户端的 HTTP 响应头之一。在此,随响应发送给客户端的 HTTP 头部中,将包含一个告知客户端响应以 JSON 字符串形式发送的头部。该头部并非强制要求。若客户端预期可能收到多种形式的响应,则提供此头部仅供参考;
- [consumes]:此处未出现。它指定了客户端的 HTTP 请求必须包含哪些 HTTP 头部,该请求才会被接受;
- 第 29 行:[@ResponseBody] 注解表明该方法生成的结果必须发送给客户端。若无此注解,该方法的响应将被视为用于选择发送给客户端的 HTML 页面的键。在 Web 服务 / JSON 中,不存在 HTML 页面;
- 第 28 行:处理后的 URL 形式为 /{a}/{b},其中 {x} 代表一个变量。变量 {a} 和 {b} 在第 30 行被赋值给方法的参数。这是通过 @PathVariable("x") 注解实现的。 请注意,{a} 和 {b} 是 URL 的组成部分,因此类型为 String。从 String 到参数类型的转换可能会失败。此时 Spring MVC 会抛出异常。总结如下:如果我在浏览器中请求 URL /100/200,第 30 行的 getAlea 方法将使用整数参数 a=100、b=200 执行;
- 第 36 行:向 [业务] 层请求一个 [a,b] 范围内的随机数。请注意,[业务] 层的 getAlea 方法可能会抛出异常;
- 第 37 行:无错误;
- 第 39 行:返回错误代码;
- 第 40 行:响应消息列表即为异常堆栈(第 46–57 行)。此处我们知道堆栈中仅包含一个异常,但我们希望演示一种更通用的方法;
- 第 43 行:将类型为 [Response<Integer>] 的响应作为 JSON 字符串返回;
1.16.1.6. Spring 项目配置
![]() |
配置 Spring 有多种方法:
- 使用 XML 文件;
- 使用 Java 代码;
- 同时使用这两种方式;
我们选择使用 Java 代码来配置我们的 Web 应用程序。以下 [Config] 类负责处理此配置:
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 = { "exemples.android.server.metier", "exemples.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);
}
// mapper jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- 第 12 行:我们告诉 Spring,它将在哪些包中查找需要管理的两个组件:
- 位于 [exemples.android.server.metier] 包中、标注了 [@Service] 的 [Metier] 组件;
- 位于 [examples.android.server.web] 包中、标注了 [@Controller] 的 [WebController] 组件;
- 第 13 行:[@EnableWebMvc] 注解允许 Spring Boot 自动处理 Spring MVC 应用程序的若干标准配置。这大大减轻了开发人员的工作量;
- 第 16、22、27 和 33 行:[@Bean] 注解与前文提到的两个注解(@Service、@Controller)类似,同样用于定义 Spring 组件(Bean)。 此处,[@Bean] 注解标注的是方法而非类,该方法的返回值即为 Spring 组件。若 [@Bean] 注解中未指定命名属性,则创建的 Spring 组件将采用被标注方法的名称;
- 第 16–20 行:定义 [dispatcherServlet] Bean。这是 Spring MVC 中预定义的名称,用于定义 MVC 应用程序的前端控制器,所有客户端请求都会经过该对象,并由其将请求分发(因此得名)到 Spring MVC 应用程序中的各个 [@Controller];
- 第 18 行:[dispatcherServlet] Bean 是 Spring MVC 提供的 [DispatcherServlet] 类的实例;
- 第 22–25 行:[servletRegistrationBean] Bean 用于定义应用程序接受哪些 URL。第 24 行中,所有 URL 均被接受;
- 第 27–30 行:[embeddedServletContainerFactory] Bean 用于定义项目依赖项中将托管 Web 应用程序的嵌入式服务器。 第 29 行指定这是一个 Tomcat 服务器,且将在 8080 端口上运行。默认情况下,该 Web 服务器的二进制文件由 Gradle 文件中的 [org.springframework.boot:spring-boot-starter-web] 依赖项提供;
1.16.1.7. 运行 Web 服务 / JSON
![]() |
该项目由以下 [Boot] 可执行类运行:
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) {
// application execution
SpringApplication.run(Config.class, args);
}
}
- [Boot] 类是一个可执行类(第 7–10 行);
- 第 9 行:静态方法 [SpringApplication.run] 是 [Spring Boot](第 4 行)的一个方法,用于启动应用程序。其第一个参数是配置该项目的 Java 类。这里,它就是我们刚才描述的 [Config] 类。第二个参数是传递给 [main] 方法(第 7 行)的参数数组;
Web 应用程序可以通过多种方式启动,包括以下几种:
![]() |
随后,控制台中会出现一些日志:
- 第 12-14 行:启动了 Tomcat 嵌入式服务器;
- 第 15-19 行:加载并配置 Spring MVC Servlet [DispatcherServlet];
- 第 20 行:检测到 Web 服务器 URL [/{a}/{b}];
现在,让我们打开浏览器并测试 Web 服务的 JSON URL:
![]() |
![]() |
![]() |
![]() |
每次,我们都会获取一个类型为 [Response<Integer>] 的对象的 JSON 表示形式。
现在,我们不再使用标准浏览器,而是使用 Chrome 浏览器的 [ Advanced Rest Client] 扩展程序(参见附录第 6.13 节):

- 在 [1] 中,请求的 URL;
- 在 [2] 中,使用 GET 请求;
- 在 [3] 中,请求已发送;

- 在 [4] 中,服务器响应的 HTTP 头部。请注意,这表明发送的文档是一个 JSON 字符串;
- 在 [5] 中,接收到的 JSON 字符串;
1.16.1.8. 生成项目的可执行 JAR 文件
在第 1.16.1.2 节中,我们展示了如何配置 Gradle 文件,以生成包含所有依赖项的应用程序可执行文件。根据当前应用程序进行调整后,该配置如下所示:
// créer un binaire avec toutes ses dépendances
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'exemples.android.server.boot.Boot'
}
baseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
要生成此可执行文件,请按照以下步骤 [1-5] 操作:
![]() | ![]() |
要运行它,请先停止正在运行的 Web 服务 [1],然后运行该归档文件 [2-4]:
![]() | ![]() |
打开浏览器并访问 URL [localhost:8080/100/200]。您应能获得与之前相同的结果。
1.16.1.9. 日志管理
运行可执行归档文件时,你会发现日志与在 IDE 中运行项目时的日志有所不同。你会看到 [DEBUG] 模式下的日志:
...
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]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: 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 - Started Boot in 1.984 seconds (JVM running for 2.206)
您可以通过在项目的 [resources] 文件夹中添加一个 [logback.xml] 文件来管理日志级别:
![]() |
该文件可能包含以下内容:
<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>
日志级别在第 12 行进行控制。如果现在重新构建可执行归档并运行它,我们只会得到 [info] 级别的日志:
...
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
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: 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 - Started Boot in 1.865 seconds (JVM running for 2.203)
1.16.2. Web 服务器 / JSON 的 Android 客户端
Android 客户端将采用以下架构:
![]() |
客户端将包含两个组件:
- 一个[展示]层(视图+活动),类似于我们在示例[Example-14]中研究过的;
- 与我们之前研究过的 [Web / JSON] 服务进行交互的 [DAO] 层。
1.16.2.1. 创建项目
我们按照第 1.4 节中的步骤,将之前的项目 [Example-14] 复制到 [Example-15] 中。最终结果如下:
![]() | ![]() |
接下来,请读者尝试创建以下项目。
1.16.2.2. Gradle 配置
![]() |
[build.gradle] 文件如下:
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// options de packaging nécessaires pour être capable de produire l'APK
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
apt "org.androidannotations:rest-spring:$AAVersion"
compile "org.androidannotations:rest-spring-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
我们仅对尚未涉及的内容进行说明:
- 第 46–47 行:插入了一个 AA 插件。[rest-spring-api] 插件允许将客户端/服务器通信委托给 AA 库;
- 第 50 行:[spring-android-rest-template] 库是 AA 用于处理客户端/服务器通信的库。版本 [2.0.0.M3] 是一个所谓的“里程碑”版本,在常规的 Maven 仓库中无法找到。因此,我们必须在第 56–59 行中指定要使用的仓库(第 58 行)以查找该库;
- 第 51 行:一个 JSON 库;
- 第 33–39 行:若缺少此属性,在生成项目的 APK 二进制文件时会发生错误;
1.16.2.3. Android 应用程序清单
![]() |
需要更新 [AndroidManifest.xml] 文件。默认情况下,互联网访问功能处于禁用状态。必须使用一个特殊指令将其启用:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.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>
- 第5行:允许访问互联网;
1.16.2.4. [DAO] 层
![]() |
![]() |
1.16.2.4.1. [DAO]层的[IDao]接口
[DAO] 层的接口如下:
package exemples.android.dao;
public interface IDao {
// random number
int getAlea(int a, int b);
// URL of the web service
void setUrlServiceWebJson(String url);
// max wait time (ms) for server response
void setTimeout(int timeout);
// client wait time in milliseconds before request
void setDelay(int delay);
}
- 第 6 行:用于从该 Web 服务获取 [a,b] 范围内随机数的 Web 服务 / JSON 方法;
- 第 9 行:用于生成随机数的 Web 服务 / JSON 的 URL;
- 第 12 行:我们设置了等待服务器响应的最大超时时间;
- 第 15 行:我们在向服务器发送请求前设置超时,以便用户有时间取消请求;
1.16.2.4.2. [WebClient] 接口
![]() |
[WebClient] 接口负责处理与 Web 服务的通信。其代码如下:
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> getAlea(@Path("a") int a, @Path("b") int b);
}
- 第 12 行:[WebClient] 是一个接口,AA 库将通过我们添加的注解自行实现该接口。该接口必须实现对 Web 服务公开的 URL 或 JSON 的调用:
// random number
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
- 第 11 行:[@Rest] 注解是一个 AA 注解。[converters] 属性的值是一个转换器数组。在此,[MappingJackson2HttpMessageConverter.class] 转换器确保当服务器发送 JSON 字符串时,该字符串会被自动反序列化。 因此,我们在第 (d) 行看到,URL [/{a}/{b}] 返回的是 String 类型,实际上是一个 JSON 字符串(第 b 行)。结合这一信息以及第 16 行中预期的类型,客户端的 [WebClient] 实例将把接收到的字符串反序列化为 [Response<Integer>] 类型;
- 第 15 行:一个 @Get 注解,表示必须使用 HTTP GET 方法调用该 URL。@Get 注解的参数是 Web 服务所期望的 URL 格式。只需使用服务器 [WebController] 中被调用方法的 @RequestMapping 注解(第 b 行)中的 [value] 参数即可。 第 16 行中,大括号 {} 包围着必须传递给方法参数的 URL 参数。语法 [@Path("a") int a] 会将 URL 中的 {a} 值赋给方法的 [a] 参数。当 URL 参数和方法参数名称相同时(如本例),我们可以更简洁地写成 [@Path int a];
对于 HTTP POST 请求,call 方法的签名如下:
@Post("/{a}/{b}")
Response<Integer> getAlea(@Body T body, @Path("a") int a, @Path("b") int b);
[@Body] 注解用于指定提交的值。该值将被自动序列化为 JSON。在服务器端,我们将拥有以下接口定义:
// 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) {
- 第 2 行:指定预期接收 HTTP POST 请求,且请求正文(提交的对象)必须以 JSON 字符串形式传输(consumes 属性);
- 第 4 行:提交的值将通过方法的 [@RequestBody T body] 参数获取;
让我们回到 [WebClient] 类的代码:
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
- 我们需要能够指定要联系的 Web 服务的 URL。这是通过继承 AA 提供的 [RestClientRootUrl] 接口来实现的。该接口提供了一个 [setRootUrl(urlServiceWeb)] 方法,允许我们设置要联系的 Web 服务的 URL;
- 此外,为了限制响应等待时间,我们需要控制对 Web 服务的调用。为此,我们扩展了 [RestClientSupport] 接口,该接口提供了 [setRestTemplate] 方法,它将允许我们:
- 自行创建 [RestTemplate] 对象,该对象用于管理客户端与服务端之间的交互;
- 配置该对象以设置最大响应超时时间;
1.16.2.4.3. [Response] 类
[IDao] 接口的 [getAlea] 方法会返回如下所示的 [Response] 类型的响应:
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;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
这是服务器端已使用的 [Response] 类(参见第 1.16.1.5 节)。实际上,从编程角度来看,这就像客户端的 [DAO] 层正在与 Web 服务的 [WebController] 直接通信:
![]() |
客户端与服务器之间的网络通信,以及客户端侧 Java 对象的序列化/反序列化,对程序员而言都是透明的。
1.16.2.4.4. [DAO]层的实现
![]() |
[IDao] 接口由以下 [Dao] 类实现:
package exemples.android.dao;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.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 {
// service customer REST
@RestService
protected WebClient webClient;
// mapper jSON
private ObjectMapper mapper = new ObjectMapper();
// timeout before request execution
private int delay;
// interface IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
...
}
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
...
}
@Override
public void setTimeout(int timeout) {
...
}
@Override
public void setDelay(int delay) {
this.delay = delay;
}
}
- 第 15 行:我们使用 [@EBean] 注解标注 [Dao] 类,将其转换为 AA Bean,以便在其他地方进行注入;
- 第 19–20 行:我们注入了之前定义的 [WebClient] 接口的实现。该注入由 [@RestService] 注解处理;
- 其余方法实现了 [IDao] 接口(第 27–46 行);
[setTimeout] 方法
[setTimeout] 方法如下:
@Override
public void setTimeout(int timeout) {
// set the client request timeout REST
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
// we build the restTemplate
RestTemplate restTemplate = new RestTemplate(factory);
// set the jSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// set the restTemplate of the web client
webClient.setRestTemplate(restTemplate);
}
- [WebClient] 接口将由类 AA 通过 Gradle 依赖项 [org.springframework.android:spring-android-rest-template] 实现。[spring-android-rest-template] 通过 [RestTemplate] 类实现客户端与 Web/JSON 服务器的通信;
- 第 4 行:[SimpleClientHttpRequestFactory] 类由 [spring-android-rest-template] 依赖项提供。它允许我们设置服务器响应的最大超时时间(第 5-6 行);
- 第 8 行:我们创建 [RestTemplate] 对象,它将作为与 Web 服务通信的通道。我们将刚刚创建的 [factory] 对象作为参数传递给它;
- 第 10 行:客户端与服务器的交互可以采取多种形式。通信通过文本行进行,我们必须告知 [RestTemplate] 对象如何处理每行文本。为此,我们为其提供转换器——即能够处理文本行的类。转换器的选择通常基于伴随文本行的 HTTP 头部。 在此,我们知道接收的仅是 JSON 格式的文本行。此外,正如我们在第 1.16.1.7 节中所见,服务器发送了 HTTP 头:
Content-Type: application/json;charset=UTF-8
第 10 行:[RestTemplate] 的唯一转换器将是使用 [Jackson] 库实现的 JSON 转换器。关于这些转换器有一个特殊之处:AA 要求我们将其也包含在 [WebClient] 注解中:
@Rest(converters = {MappingJacksonHttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
第 1 行:尽管我们已经在代码中指定了转换器,但系统仍要求我们显式指定转换器。
- 第 12 行:通过这种方式构造的 [RestTemplate] 对象会被注入到 [WebClient] 接口的实现中,而正是这个对象将负责处理客户端与服务端之间的通信;
方法 [getAlea]
[getAlea] 方法如下:
@Override
public int getAlea(int a, int b) {
// service execution
Response<Integer> info;
DaoException ex;
try {
// waiting
waitSomeTime(delay);
// service execution
info = webClient.getAlea(a, b);
int status = info.getStatus();
if (status == 0) {
// we return the result
return info.getBody();
} else {
// we note the exception
ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
}
} catch (JsonProcessingException | RuntimeException e) {
// we note the exception
ex = new DaoException(e, 100);
}
// we launch the exception
throw ex;
}
...
// private methods -------------------
private void waitSomeTime(int delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 第 8 行:等待 [delay] 毫秒;
- 第 10 行:我们只需调用实现 [WebClient] 接口的类中具有相同签名的方法;
- 第 11 行:通过检查 [status] 来分析从服务器接收到的响应;
- 第 12–14 行:如果服务器端没有错误(status = 0),则返回该方法的结果;
- 第 17 行:如果发生服务器端错误(status!=0),则准备一个异常但不抛出。服务器已发送了一组错误消息。我们创建一个异常,其唯一消息为服务器消息列表的 JSON 字符串;
- 第 19–22 行:其他异常情况;
- 第 24 行:当执行到此处时,必然已发生异常。因此抛出该异常;
此代码使用的 [DaoException] 如下:
package exemples.android.dao;
import java.util.ArrayList;
import java.util.List;
public class DaoException extends RuntimeException {
// error code
private int code;
// manufacturers
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
...
}
- 第 6 行:[DaoException] 是一个未处理的异常;
方法 [setUrlServiceWebJson]
[setUrlServiceWebJson] 方法如下:
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// we set the URL of the REST service
webClient.setRootUrl(urlServiceWebJson);
}
- 第 4 行:我们使用 [WebClient] 接口的 [setRootUrl] 方法设置 Web 服务 URL。该方法的存在是因为该接口继承了 [RestClientRootUrl] 接口;
1.16.2.5. [architecture] 包
[architecture] 包包含用于构建应用程序结构的元素:
![]() |
![]() |
1.16.2.5.1. [IMainActivity] 接口
[IMainActivity] 接口列出了应用程序的 Activity 必须实现的方法:
package exemples.android.architecture;
import exemples.android.dao.IDao;
public interface IMainActivity extends IDao {
// session access
Session getSession();
// change of view
void navigateToView(int position);
// waiting
void beginWaiting();
void cancelWaiting();
// debug mode
boolean IS_DEBUG_ENABLED = true;
// response time
int TIMEOUT = 1000;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
}
- 第 5 行:[IMainActivity] 接口继承自 [IDao] 接口;
- 第 13–16 行:在之前示例中已有的方法(第 7–11 行)基础上,我们新增了两个用于管理应用程序加载界面的方法(第 14、16 行);
- 第 21 行:我们将服务器响应的最大超时时间设置为 1 秒;
1.16.2.5.2. [Utils] 类
我们在 [Utils] 类中汇总了静态实用方法,这些方法可在应用程序架构的各个部分调用:
package exemples.android.architecture;
import java.util.ArrayList;
import java.util.List;
public class Utils {
// list of exception messages - version 1
static public List<String> getMessagesFromException(Throwable ex) {
// create a list of error msgs from the exception stack
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// exception message list - version 2
static public String getMessagesForAlert(Throwable th) {
// build the text to be displayed
StringBuilder texte = new StringBuilder();
List<String> messages = getMessagesFromException(th);
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// result
return texte.toString();
}
}
- 第 9–18 行:创建一个包含在 Throwable 中的错误消息列表;
- 第 21–32 行:利用前面的方法,根据获取的消息列表构建要在 Android 提示消息中显示的文本;
- 第27–28行:对消息进行编号。最小编号(1)对应初始异常,最大编号对应异常堆栈中的最新异常;
1.16.2.5.3. 抽象类 [AbstractFragment]
[AbstractFragment] 类有两个用途:
- 确保在显示片段时,子类的 [updateFragments] 方法总会被调用,且仅调用一次;
- 将子类中可提取的状态和方法进行提取;
正是第二个目的促使我们在该类中加入了等待图像的管理操作:所有异步 Android 应用程序的组件都必须处理此类问题:
// wait management
protected void beginWaiting() {
// we set the hourglass
mainActivity.beginWaiting();
}
protected void cancelWaiting() {
// the hourglass is removed
mainActivity.cancelWaiting();
}
1.16.2.6. 视图
![]() |
1.16.2.6.1. 视图 [view1.xml]
![]() |
与前面的示例相比,视图 [view1.xml] 发生了以下变化:
![]() |
![]() |
- 在[1]中,用户必须在每次调用Web服务之前指定Web服务URL和超时时间[2];
- 在[3]中,会统计响应次数;
- 在[4]中,用户可以取消请求;
- 在[5]中,请求号码时会出现加载指示器。当所有号码接收完毕或操作被取消时,该指示器将消失;

- 在[6]中,会检查输入内容的有效性;
请读者从示例中加载文件 [vue1.xml]。在本节的剩余部分,我们将提供新组件的 ID:

按钮 [10-11] 在物理上重叠在一起。在任何给定时刻,这两个按钮中只有一个会显示出来。
1.16.2.6.2. [Vue1Fragment] 片段
![]() |
[Vue1Fragment] 片段的结构如下:
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 exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.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.textViewErreurUrl)
TextView txtMsgErreurUrlServiceWeb;
@ViewById(R.id.editTextDelay)
EditText edtDelay;
@ViewById(R.id.textViewErreurDelay)
TextView textViewErreurDelay;
@ViewById(R.id.lst_reponses)
ListView listReponses;
@ViewById(R.id.txt_Reponses)
TextView infoReponses;
@ViewById(R.id.edt_nbaleas)
EditText edtNbAleas;
@ViewById(R.id.edt_a)
EditText edtA;
@ViewById(R.id.edt_b)
EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
TextView txtErrorIntervalle;
@ViewById(R.id.btn_Executer)
Button btnExecuter;
@ViewById(R.id.btn_Annuler)
Button btnAnnuler;
...
// local data
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
@AfterViews
void afterViews() {
// memory
afterViewsDone=true;
// initially no error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// hidden [Cancel] button
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
// list of answers
reponses = new ArrayList<>();
}
...
- 第 24–49 行:对视图组件 [view1.xml] 的引用(第 20 行);
- 第 55–69 行:当第 24–49 行的引用初始化完成后执行的 [@AfterViews] 方法;
- 第 58 行:切勿遗漏此行——这是片段生命周期所必需的;
- 第 60–63 行:隐藏错误消息;
- 第 65–66 行:隐藏 [Cancel] 按钮(第 65 行)并显示 [Execute] 按钮(第 66 行)。请注意,这两个按钮在物理上重叠;
- 第 68 行:第 52 行中的字段将包含响应列表视图(ListView)要显示的字符串列表;
在 [@AfterViews] 方法之后,将立即执行以下 [updateFragment] 方法:
@Override
protected void updateFragment() {
// create the response list adapter
adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
- 第 4-5 行:创建用于答案的 ListView 适配器。将其存储在实例变量中,以便类中的其他方法可以使用;
点击 [Execute] 按钮将触发以下方法的执行:
// seizures
private int nbAleas;
private int a;
private int b;
private String urlServiceWebJson;
private int delay;
// local data
private int nbInfos;
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
private boolean hasBeenCanceled;
@Click(R.id.btn_Executer)
protected void doExecuter() {
// delete previous answers
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// reset the response counter to 0
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// test the validity of entries
if (!isPageValid()) {
return;
}
// activity initialization
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// we ask for the random numbers
for (int i = 0; i < nbAleas; i++) {
getAlea(a, b);
}
// we start waiting
beginWaiting();
}
@Background(id = "alea")
void getAlea(int a, int b) {
// do as little as possible here
// in any case no display - these must take place in the UiThead
try {
// the result is displayed in the UiThread
showInfo(mainActivity.getAlea(a, b));
} catch (RuntimeException e) {
// the exception is displayed in the UiThread
showAlert(e);
}
}
- 第 17–18 行:我们清空了之前从服务器获取的响应列表。为此,在第 17 行,我们清空了与 ListView 适配器关联的数据源 [reponses];
- 第 19 行:一个布尔变量,用于标记用户是否取消了请求;
- 第 21–22 行:我们显示一个初始值为零的响应计数器;
- 第24–26行:我们从[2–6]行中检索条目并验证其有效性。如果其中任何一个无效,则终止该方法(第25行),并将用户返回至视觉界面;
- 第 28-29 行:如果所有输入数据均有效,则将 Web 服务 URL(第 28 行)和每次服务调用前的等待时间(第 29 行)传递给 Activity。这些信息是 [DAO] 层所必需的,请注意,与该层进行通信的是 Activity;
- 第 31–33 行:通过第 39 行的 [getAlea] 方法逐个请求随机数;
- 第 38 行:[getAlea] 方法标注了 AA [@Background] 注解,这意味着它将在与视觉界面运行不同的线程(执行流、进程)中执行。事实上,任何互联网调用都必须在与视觉界面不同的线程中执行。因此,在任何给定时刻,可能存在多个线程:
- 一个用于显示用户界面(UI)并管理其事件的线程,
- 以及 [nbAleas] 线程,每个线程都会向 Web 服务请求一个随机数。这些线程是异步启动的:UI 线程会启动一个 [getAlea] 线程(第 32 行),该线程向 Web 服务请求随机数,但不会等待其完成。它将通过事件通知来获知请求已完成。 因此,[nbAleas] 线程将并行启动。也可以配置应用程序,使其每次只启动一个线程。在这种情况下,会有一个待执行的线程队列;
第 38 行:[id] 参数为生成的线程分配名称。此处,[nbAleas] 线程均使用相同名称 [alea]。这将使我们能够同时取消所有线程。若未处理线程取消,该参数为可选;
- 第 44 行:调用 Activity 的 [getAlea] 方法。因此,该方法将在与 UI 不同的独立线程中执行。该线程将调用 Web 服务,且不会等待响应。稍后将通过事件通知其响应已可用。此时,在第 44 行,将调用 [showInfo] 方法,并将收到的响应作为参数传入;
- 第 45–47 行:执行 Web 请求可能会抛出异常。随后,我们要求将该异常的错误消息显示在提示框中;
- 第 35 行:我们等待结果:
- 将显示加载指示器;
- [取消]按钮将替换[执行]按钮。由于启动的线程是异步的,UI线程不会等待它们,因此第35行会在它们完成之前执行。 一旦 [beginWaiting] 方法执行完毕,UI 即可再次响应用户操作,例如点击 [取消] 按钮。如果启动的线程是同步的,则只有在所有线程都完成后才会执行第 35 行。此时再取消它们就不再有意义了;
[showInfo] 方法如下:
@UiThread
protected void showInfo(int alea) {
if (!hasBeenCanceled) {
// one more piece of information
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// are we done?
if (nbInfos == nbAleas) {
// we end the wait
cancelWaiting();
}
// we add the information to the list of answers
reponses.add(0, String.valueOf(alea));
// we display the answers
adapterReponses.notifyDataSetChanged();
}
}
- [showInfo] 方法是在标注了 [@Background] 的 [getAlea] 线程中调用的。该方法将更新用户界面。它只能在 UI 线程中运行才能实现这一点。这就是第 1 行 [@UiThread] 注解的含义;
- 第 2 行:该方法接收一个随机数;
- 第 3 行:仅当用户未取消请求时,才会执行该方法的主体;
- 第 5–6 行:响应计数器递增并显示;
- 第 8–11 行:如果已收到所有预期的响应,则终止等待(等待信号结束;[Execute] 按钮替换 [Cancel] 按钮);
- 第 12–15 行:将接收到的随机数添加到 [ListView listReponses] 组件显示的响应列表中,并刷新列表;
[showAlert] 方法如下:
@UiThread
protected void showAlert(Throwable th) {
if (!hasBeenCanceled) {
// we cancel everything
doAnnuler();
// we display it
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
这里的逻辑与 [showInfo] 方法类似:
- 第 1 行:必须使用 [@UiThread] 注解;
- 第 2 行:该方法接收发生的异常;
- 第 3 行:仅当用户未取消请求时,该方法才会执行;
- 第 5 行:取消用户的请求,效果如同用户亲自点击了 [Cancel] 按钮;
- 第 7 行:使用 Android 的 [AlertDialog] 类显示提示框:
- [activity]:是存储在父类 [AbstractFragment] 中的 [Activity] 类型活动;
- [setTitle]:设置提示框的标题 [1];
- [setMessage]:设置提示窗口显示的消息 [2];
- [setNeutral]:设置用于关闭提示窗口的按钮 [3];
- [show]:请求显示提示窗口;
![]() |
点击 [取消] 按钮将通过以下方法进行处理:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// memory
hasBeenCanceled=true;
// the asynchronous task is cancelled
BackgroundExecutor.cancelAll("alea", true);
// end of wait
cancelWaiting();
}
- 第 4 行:请注意用户已取消其请求;
- 第 6 行:取消所有标识符为 [alea] 的任务。第二个参数 [true] 表示即使任务已启动,也必须将其取消。标识符 [alea] 是用于修饰片段 [getAlea] 方法的标识符(见下文第 1 行):
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
注:结果发现,[doAnnuler] 方法代码的第 6 行行为异常。这就是我们添加布尔变量 [hasBeenCanceled] 的原因。事实上,如果发生异常(服务器宕机),而我们之前请求了 n 个随机数,那么提示窗口将会出现 n 次。
1.16.2.7. [MainActivity] 活动
![]() |
1.16.2.7.1. [activity-main.xml] 视图
![]() |
与前面的示例相比,我们在与 [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">
<!-- image d'waiting -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- image d'waiting -->
</android.support.design.widget.AppBarLayout>
...
- 第 17-21 行:占位图;
1.16.2.7.2. [MainActivity] 活动
[MainActivity] 与 [Example-14] 中的相比变化不大。首先,我们将 [DAO] 层注入其中:
// dao injection
@Bean(Dao.class)
protected IDao dao;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// set the [DAO] layer
setTimeout(TIMEOUT);
}
- 第 2-3 行:通过 AA 注解注入 [DAO] 层;
- 第 5-13 行:注入后执行的代码;
- 第12行:设置[DAO]层的超时
此外,[MainActivity] 活动必须实现 [IMainActivity] 接口,而该接口本身继承自 [IDao] 接口:
// implémentation IMainActivity --------------------------------------------------------------------
@Override
public void navigateToView(int position) {
// the position view is displayed
if (mViewPager.getCurrentItem() != position) {
// fragment display
mViewPager.setCurrentItem(position);
}
}
// hold image management
public void cancelWaiting() {
loadingPanel.setVisibility(View.INVISIBLE);
}
public void beginWaiting() {
loadingPanel.setVisibility(View.VISIBLE);
}
// implémentation IDao --------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// execution
return dao.getAlea(a, b);
}
@Override
public void setDelay(int delay) {
dao.setDelay(delay);
}
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
1.16.2.8. 运行项目
启动 Web 服务(第 1.16.1.7 节),然后启动 Android 客户端:

要了解 [1] 中应输入什么内容,请按照以下步骤操作。打开命令提示符,并输入以下命令:
C:\Program Files\Console2>ipconfig
Configuration IP de Windows
Carte réseau sans fil Connexion au réseau local* 3 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet VirtualBox Host-Only Network :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::e481:1583:cd2a:c47%27
Adresse IPv4. . . . . . . . . . . . . .: 192.168.82.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet VirtualBox Host-Only Network #2 :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::8191:14ad:407d:b840%54
Adresse IPv4. . . . . . . . . . . . . .: 192.168.64.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet Ethernet :
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Adresse IPv6 de liaison locale. . . . .: fe80::d972:ad53:3b8a:263f%28
Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
Masque de sous-réseau. . . . . . . . . : 255.255.0.0
Passerelle par défaut. . . . . . . . . : 172.19.0.254
Carte réseau sans fil Wi-Fi :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . : uang ad.univ-angers.fr univ-angers.fr
如果您已安装 [GenyMotion],VirtualBox 虚拟机已为您的计算机添加了 IP 地址(第 10 行和第 18 行)。这些地址特别方便,因为它们不会被 Windows 防火墙拦截。 第 30 行显示了您计算机在本地网络上的 IP 地址。要使用该地址,通常需要禁用 Windows 防火墙。如果您连接到 Wi-Fi 网络,请使用 Wi-Fi 地址,并且在此情况下,如果启用了防火墙,也请将其禁用。
请在以下情况下测试该应用程序:
- 在 [1000, 2000] 范围内生成 100 个随机数,且不设置超时;
- 生成2000个[10000, 20000]范围内的随机数,不设置超时,并在生成完成前取消等待;
- 在 [100, 200] 范围内生成 5 个随机数,等待时间为 5000 毫秒,并在生成完成前取消等待;
1.16.2.9. 取消处理
为了追踪用户请求取消或异常触发取消时的情况,我们在 [IDao] 接口中添加了以下方法(参见第 1.16.2.4.1 节):
package exemples.android.dao;
public interface IDao {
...
// debug mode
void setDebugMode(boolean isDebugEnabled);
}
在 [Dao] 类中,我们添加以下代码:
// debug mode
private boolean isDebugEnabled;
// class name
private String className;
..
// manufacturer
public Dao() {
// class name
className = getClass().getSimpleName();
}
...
// interface IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// service execution
Response<Integer> info;
...
@Override
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
- 第 9 行:我们记录类名;
- 第 16–18 行:每次调用 [getAlea] 方法时,我们都会写入一条日志;
此外,在 [Vue1Fragment] 片段中,我们添加了以下日志:
@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 reçue");
}
...
}
}
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
...
}
每当 [Vue1Fragment] 片段从 [DAO] 层接收信息时,都会生成一条日志。此外,当调用 [doAnnuler] 方法时,该事件也会被记录。
测试 1
尽管服务器尚未启动,我们仍请求了 5 个数字。我们获得了以下日志:
- 第 1–5 行:[Dao] 类的 [getAlea] 方法被调用五次。请注意,这些是由 [VueFragment] 片段发起的异步调用,且该片段不会等待调用结果;
- 第 7 行:首次 HTTP 请求已发出,且 [VueFragment] 片段已收到首个异常;
- 第 8 行:随后它请求取消所有请求;
- 第 9–12 行:然而,我们可以看到它收到了接下来的四个异常。因此,待处理的异步请求均已执行完毕;
测试 2
现在,让我们启动服务器并请求 5 个数字,设置 5 秒延迟,然后在延迟结束前点击 [取消]。日志如下:
- 第 1-5 行:[Dao] 类的 [getAlea] 方法被调用五次;
- 第 7 行:用户请求取消这些请求;
- 第 8 行:我们看到 [Vue1_Fragment] 接收了 5 个值。再次,待处理的异步请求均已执行完毕;
这就是为什么我们需要管理一个布尔变量 [hasBeenCanceled],以避免在收到取消请求时显示任何内容。在取消代码中:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// memory
hasBeenCanceled = true;
// the asynchronous task is cancelled
BackgroundExecutor.cancelAll("alea",true);
// end of wait
cancelWaiting();
}
第 10 行的代码未按预期运行。这可能是因为异步任务共享了同一个带有 [@Background] 注解的方法:
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
1.17. 示例-16:使用 RxAndroid 处理异步操作
接下来,我们将使用名为 RxJava [http://reactivex.io/] 的库及其针对 Android 环境的衍生版本 [RxAndroid] 来管理 Android 应用程序所需的异步操作。为此,我们将参考课程 [RxJava 入门:在 Swing 和 Android 环境中的应用]。
1.17.1. 创建项目
我们将 [示例-1] 项目复制为 [示例-16]:
![]() | ![]() |
1.17.2. Gradle 配置
![]() |
在 [build.gradle] 文件中,我们添加了对 [RxAndroid] 库的依赖:
dependencies {
...
compile 'io.reactivex:rxandroid:1.2.0'
}
1.17.3. [DAO] 层
![]() |
1.17.4. [IDao] 接口
[IDao] 接口变为如下形式:
package exemples.android.dao;
import rx.Observable;
public interface IDao {
// random number
Observable<Integer> getAlea(int a, int b);
// URL of the web service
void setUrlServiceWebJson(String url);
// max wait time (ms) for server response
void setTimeout(int timeout);
// client wait time in milliseconds before request
void setDelay(int delay);
// debug mode
void setDebugMode(boolean isDebugEnabled);
}
- 第 8 行:[getAlea] 方法现在返回 RxJava 库中的 [Observable] 类型(第 3 行)。其原理如下:
一个由 Observable<T> 类型元素组成的流,由一个或多个 Subscriber<T> 类型的订阅者(观察者、消费者)进行观察。 RxJava 库允许 Observable<T> 流在线程 T1 中运行,其 Subscriber<T> 观察者在线程 T2 中运行,而开发者无需担心管理这些线程的生命周期,也无需处理诸如线程间数据共享和执行全局任务时的线程同步等自然而然的难题。因此,它简化了异步编程。
1.17.5. [AbstractDao] 类
我们将基于以下 [AbstractDao] 类派生 [Dao] 类:
package exemples.android.dao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;
public abstract class AbstractDao {
// mapper jSON
private ObjectMapper mapper = new ObjectMapper();
// méthodes protégées ----------------------------------------------------------
// 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();
// mistake?
int status = response.getStatus();
if (status != 0) {
// we note the exception
ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
} else {
// we issue the answer
subscriber.onNext(response.getBody());
// we signal the end of the observable
subscriber.onCompleted();
}
} catch (JsonProcessingException | RuntimeException e) {
// we note the exception
ex = new DaoException(e, 100);
}
// exception?
if (ex != null) {
// we issue the exception
subscriber.onError(ex);
}
}
});
}
}
- [AbstractDao] 类的主要组成部分是一个泛型方法 [getResponse],用于从服务器获取 [Response<T>],其中 T 是 HTTP 客户端所需的结果类型(此处为 Integer);
- 第 20 行:泛型方法 [getResponse] 的唯一参数是第 15–17 行中定义的泛型接口 [IRequest<T>] 的实例。该接口仅包含一个方法 [getResponse],正是该方法返回所需的 [Response<T>];
- 基于前两点,[AbstractDao] 类可作为任何客户端 [Dao] 层的父类,该层对应的服务器返回 [Response<T>] 类型的响应;
- 第 20 行:泛型方法 [getResponse] 返回类型 [Observable<T>],该类型代表 HTTP 客户端实际期望的结果(此处为类型 Observable<Integer>);
- 第 22–51 行:静态方法 [rx.Observable.create] 创建一个 [Observable] 类型;
- 第 22 行:该方法的唯一参数是类型 [rx.Observable.OnSubscribe<T>] 的实例,这是一个包含以下方法的接口:
- [onNext(T element)]:允许向观察者发布类型为 T 的元素;
- [onError(Throwable th)]:允许向观察者发出异常;
- [onCompleted]:允许向观察者指示数据发布已结束;
类型 [Observable<T>] 遵循以下约束:
- 它使用 [onNext(T element)] 方法发出元素;
- 一旦没有更多元素可向观察者发布,必须立即且仅调用一次 [onCompleted] 方法;
- 如果已调用 [onError(Throwable th)] 方法,则不会调用 [onCompleted] 方法;
在我们的示例中:
- 观察者将是片段 [Vue1Fragment]。正是该观察者消费了 [Observable<T>] 发出的元素(元素或异常);
- 创建的 [Observable<T>] 类型仅会发布一个元素(第 37 行);
- 第 29 行:向服务器发起同步 HTTP 请求并获取 [Response<T>] 类型。此 HTTP 请求由作为泛型方法 [getResponse] 参数传递的 [IRequest] 类型处理;
- 第 31 行:获取响应状态;
- 第 32–34 行:如果该状态指示存在错误,则准备一个异常;
- 第 36–39 行:如果状态不是错误,则发送客户端实际期望的响应(第 37 行),并通知观察者将不再有进一步的输出(第 39 行);
- 第 41–44 行:如果 HTTP 请求以异常结束,则将其记录到日志中;
- 第 46–49 行:如果异常 [ex] 不为空,则将其发布给观察者。此处无需调用 [onCompleted] 方法来告知观察者不会再发布其他元素。这是隐含的;
从上述说明中得出的关键要点是:
- 泛型方法 [<T> Observable<T> getResponse(final IRequest<T> request)] 返回一个类型 [Observable<T>],该类型会发出一个类型为 T 的单个元素,或者抛出一个异常;
- 该方法的唯一参数是一个类型 [IRequest<T>],其唯一方法 [getResponse()] 执行 HTTP 请求并返回类型 [Response<T>];
1.17.6. [Dao] 类
[Dao] 类的演变如下:
@EBean
public class Dao extends AbstractDao implements IDao {
// service customer REST
@RestService
protected WebClient webClient;
// timeout before request execution
private int delay;
// debug mode
private boolean isDebugEnabled;
// class name
private String className;
// manufacturer
public Dao() {
// class name
className = getClass().getSimpleName();
}
// interface IDao -------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// web client execution
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// waiting
waitSomeTime(delay);
// synchronous HTTP call
return webClient.getAlea(a, b);
}
});
}
...
- 第 2 行:[Dao] 类继承自 [AbstractDao] 类;
- 第 24 行:[getAlea] 方法现在返回类型 [Observable<Integer>];
- 第 30 行:调用父类的泛型方法 [getResponse]。该方法接收一个类型为 [IRequest<Integer>] 的参数;
- 第 32–37 行:实现了 [IRequest<Integer>] 接口;
- 第 36 行:与之前一样,通过 AA [webClient] 接口发起 HTTP 请求。我们知道将获取类型 [Response<Integer>],这确实是方法 [IRequest<Integer>.getResponse()] 必须返回的类型;
- 第 36 行:此处我们使用了一项称为闭包的功能:即在实例创建时,能够将其外部的值封装到实例内部,本例中即第 24 行中的 [a, b] 值。正是这一特性使得方法 [IRequest<Integer>.getResponse()] 无需参数。这些值已被嵌入到方法体中。 而在通常情况下,我们需要将方法的参数从 (a, b) 改为 (x, y),但在此处,我们创建了一个新的 [IRequest<Integer>] 实例,将 x 和 y 的值封装其中;
1.17.7. [MainActivity] 类
实现 [IDao] 接口的 [MainActivity] 类演变如下:
// implémentation IDao --------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(int a, int b) {
// execution
return dao.getAlea(a, b);
}
1.17.8. [Vue1Fragment] 类
[Vue1Fragment] 类的演变过程如下:
@Click(R.id.btn_Executer)
protected void doExecuter() {
// delete previous answers
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// reset the response counter to 0
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// test the validity of entries
if (!isPageValid()) {
return;
}
// activity initialization
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// we ask for the random numbers
getAleasInBackground(a, b);
// we start waiting
beginWaiting();
}
- 第 18 行:调用 [getAleasInBackground] 方法请求随机数,该方法之所以这样命名,是因为随机数将在与 UI 线程不同的线程中请求;
private int nbReponses = 0;
// subscriptions to observables
private List<Subscription> abonnements;
// annotation [Background] unnecessary
void getAleasInBackground(int a, int b) {
// initially no response and no subscriptions
nbReponses = 0;
abonnements.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.getAlea(a, b).subscribeOn(Schedulers.io()));
}
// the cumulative observable will be observed on the UI thread
response = response.observeOn(AndroidSchedulers.mainThread());
try {
// the observable is executed
abonnements.add(response.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
// we add the information to the list of answers
showInfo(alea);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
// error message
showAlert(th);
// end waiting
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// end waiting
cancelWaiting();
}
}));
} catch (RuntimeException e) {
// the exception is displayed in the UiThread
showAlert(e);
}
}
- 第 3 行:可观察对象拥有订阅者。订阅者与其所观察的过程之间的关联称为订阅。在此,我们仅有一个被观察的过程和一个订阅者。因此,我们将只有一个订阅。为了说明原理,我们将其视为可能存在多个被观察的过程,由不同的观察者进行监控,这将导致多个订阅;
- 第 11–18 行:我们配置被观察的过程(可观察对象)。需要明确的是,这仅是配置:该过程并未被执行;
- 第 11 行:我们从一个空的可观察对象开始,即一个不发出任何信号的可观察对象;
- 第 14–16 行:向这个空可观察对象添加 [nbAleas] 个可观察对象,它们将分别对应 [nbAleas] 个返回 [nbAleas] 个随机数的 HTTP 请求;
- 第 15 行:与之前一样,随机数 i 是从 [MainActivity] 类中请求的。需要明确的是,此时尚未执行任何 HTTP 请求。方法 [mainActivity.getRandom(a, b)] 被调用并返回一个 [Observable<Integer>]。这是一个一旦启动就会被观察的过程;
- 第 15 行:[subscribeOn(Schedulers.io())] 方法要求该过程在 I/O 线程上执行(当其被执行时)。RxJava 库提供了不同类型的线程。I/O 线程适用于 HTTP 请求;
- 第 15 行:可观察对象 #i 与第 11 行的初始可观察对象合并:通过 [nbAleas] 个各自发出一个元素的可观察对象,我们创建了一个将发出 [nbAleas] 个元素的可观察对象。这就是将被观察的对象。 当构成该可观察对象的所有可观察对象均已发出各自的 [onCompleted] 通知时,该可观察对象会发出 [onCompleted] 通知。这将使我们无需像在上一版本中那样通过计数响应来判断是否已收到所有预期数值;
- 第 18 行:至此,我们已配置了一个由 [nbAleas] 个可观察对象组成的可观察对象,每个可观察对象都在一个 I/O 线程上运行;
- 第 18 行:[observeOn(AndroidSchedulers.mainThread())] 方法指定了应在哪个线程上观察该可观察对象发出的值。 这里,线程 [AndroidSchedulers.mainThread())] 属于 RxAndroid 库,而非 RxJava。它指的是 UI 线程,也称为事件循环。这一点很重要:在 Android 应用中,修改 UI 组件只能在 UI 线程上进行;否则会引发异常;
- 第 19–45 行:既然待观察的进程已配置完毕,我们就执行它;
- 第 21 行:[Observable.subscribe] 操作会启动被观察流程的执行。该操作将启动之前配置的 [nbAleas] 异步流程。这些流程的结果将自动在 UI 线程上提供给观察者;
- 请注意,该可观察对象会发出三种类型的事件:
- [onNext]:当它发布一个元素时;
- [onError]:当它遇到异常时;
- [onCompleted]:当它发出将不再发布事件的信号时;
[Observable.subscribe] 方法接受三个对象作为参数:[Action1<Integer>, Action1<Throwable>, Action0],其 [call] 方法用于处理这三种事件;
- 第 21–27 行:第一个类型为 [Action1<Integer>] 的参数用于处理 [onNext] 事件。其 [call] 方法接收可观察对象发出的元素(第 23 行);
- 第 25 行:我们复用了前一个示例中的 [showInfo] 方法;
- 第 27–35 行:类型为 [Action1<Throwable>] 的第二个参数用于处理 [onError] 事件。其 [call] 方法接收可观察对象发出的异常(第 29 行);
- 第 31 行:我们复用了前一个示例中的 [showAlert] 方法;
- 第 33 行:我们启动取消用户请求的流程。这包括取消当前正在运行的所有可观察对象;
- 第 35–41 行:类型为 [Action0] 的第三个参数用于处理 [onCompleted] 事件。其 [call] 方法不带参数;
- 第 39 行:取消等待;
[showInfo] 方法的演变如下:
// annotation [UiThread] 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 information
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// we add the information to the list of answers
reponses.add(0, String.valueOf(alea));
// display answers
adapterReponses.notifyDataSetChanged();
}
}
该方法有两处更改:
- 第 1 行:我们移除了 AA 注解 [@UiThread];
- 我们不再通过计数响应来判断是否停止等待。现在由可观察对象的 [onCompleted] 事件提供此信息;
[showAlert] 方法的修改如下:
// annotation [UiThread] unnecessary
protected void showAlert(Throwable th) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
if (!hasBeenCanceled) {
// we cancel everything
doAnnuler();
// we display it
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
- 唯一的改动在第 1 行:我们移除了 AA 注解 [@UiThread];
最后,[doAnnuler] 方法的修改如下:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// memory
hasBeenCanceled = true;
// asynchronous tasks are cancelled
if (abonnements != null) {
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
}
// end of wait
cancelWaiting();
}
- 第 12 行:取消订阅,从而停止对相关进程的监视;
1.17.9. 执行
启动 Web 服务(第 1.16.1.7 节),启动 Android 客户端,并重复您在上一示例中执行的测试(第 1.16.2.8 节)。
1.17.10. 处理取消
我们重复与前一个示例相同的测试(第 1.16.2.9 节)。
测试 1
尽管服务器尚未启动,我们仍请求了5个数字。我们获得了以下日志:
第 7 行之后,没有更多的日志,这表明观察者(Vue1Fragment)不再从被观察进程接收通知。
测试 2
现在,让我们启动服务器并请求 5 个数字,设置 5 秒延迟,然后在延迟结束前点击 [取消]。日志如下:
第 6 行之后,没有更多的日志,这表明观察者(Vue1Fragment)不再接收来自被观察进程的通知。
这是取消操作的预期行为。因此,我们可以从 [Vue1Fragment] 代码中移除上一个示例中引入的布尔变量 [hasBeenCanceled],因为取消操作当时并未按预期工作。
可观察对象被取消后,观察者不再接收通知,但这并不意味着 HTTP 请求本身也被取消了。为了验证这一点,我们将 [Dao] 类修改如下:
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// web client execution
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// waiting
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),"erreur désérialisation jSON");
}
}
return response;
}
});
}
- 第 15–21 行:我们记录第 14 行 HTTP 请求的结果;
测试 #2 的日志如下:
- 第1-5行:发出了5个请求;
- 第 6 行:用户取消了请求;
- 第 7-11 行:我们成功接收了这五个 HTTP 请求的响应。但是,由于可观察对象已被取消,这些元素不会传递给观察者;
1.17.11. 结论
在本文档的剩余部分中,将使用 RxAndroid 库而非 AA 库来实现客户端/服务器应用程序,原因如下:
- RxAndroid 可在不使用 AA 的 Android 应用中使用;
- RxAndroid 不仅能促进异步操作,还提供了多种方法,可基于另一个可观察对象创建新的可观察对象。这些方法在 AA 中没有对应功能;
- 一旦尝试派生带有 AA 注解的类(例如片段),就会出现严重问题。届时将不得不放弃 AA,转而采用方案 1 进行异步编程;
有兴趣进一步探索 RxAndroid 库功能的读者,可参考文档《RxJava 入门:在 Swing 和 Android 环境中的应用》。该文档在未使用 AA 库的情况下使用了 RxAndroid。
1.18. 示例-17:数据输入组件
我们将创建一个新项目,以演示数据输入表单中常用的某些组件。
1.18.1. 创建项目
我们将 [示例-13] 项目复制为 [示例-17]:
![]() | ![]() |
新项目将仅包含一个视图 [view1.xml]。因此,我们将删除视图 [view2.xml] 及其关联的片段 [View2Fragment] [2]。我们将在 [MainActivity] 的片段管理器中反映这一变更:
// our fragment manager to be redefined for each application
// must define the following methods: getItem, getCount, getPageTitle
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private final Fragment[] fragments = {new Vue1Fragment_()};
....
}
重新运行该项目。它应该像之前一样显示视图 #1。我们将以此项目为基础进行后续工作。
1.18.2. 表单的 XML 视图
![]() |
由 [vue1.xml] 文件生成的视图如下:

该视图的 XML 代码如下:
<?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/textViewFormulaireTitre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="10dp"
android:layout_marginTop="30dp"
android:text="@string/titre_vue1"
android:textSize="30sp"/>
<Button
android:id="@+id/formulaireButtonValider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/TextViewFormulaireCombo"
android:layout_below="@+id/TextViewFormulaireCombo"
android:layout_marginTop="30dp"
android:text="@string/formulaire_valider"/>
<TextView
android:id="@+id/textViewFormulaireCheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireTitre"
android:layout_below="@+id/textViewFormulaireTitre"
android:layout_marginTop="30dp"
android:text="@string/formulaire_checkbox"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireCheckBox"
android:layout_below="@+id/textViewFormulaireCheckBox"
android:layout_marginTop="30dp"
android:text="@string/formulaire_radioButton"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireSeekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireRadioButton"
android:layout_below="@+id/textViewFormulaireRadioButton"
android:layout_marginTop="30dp"
android:text="@string/formulaire_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/formulaire_saisie"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireBool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireEdtText"
android:layout_below="@+id/textViewFormulaireEdtText"
android:layout_marginTop="30dp"
android:text="@string/formulaire_bool"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireDate"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:layout_alignLeft="@+id/textViewFormulaireBool"
android:layout_below="@+id/textViewFormulaireBool"
android:layout_marginTop="50dp"
android:gravity="center"
android:text="@string/formulaire_date"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireMultilignes"
android:layout_width="150dp"
android:layout_height="100dp"
android:gravity="center"
android:layout_alignBaseline="@+id/textViewFormulaireTitre"
android:layout_alignParentTop="true"
android:layout_marginLeft="400dp"
android:layout_toRightOf="@+id/textViewFormulaireTitre"
android:text="@string/formulaire_multilignes"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireTime"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:gravity="center"
android:layout_alignLeft="@+id/textViewFormulaireMultilignes"
android:layout_below="@+id/textViewFormulaireMultilignes"
android:layout_marginTop="30dp"
android:text="@string/formulaire_time"
android:textSize="20sp"/>
<TextView
android:id="@+id/TextViewFormulaireCombo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireTime"
android:layout_below="@+id/textViewFormulaireTime"
android:layout_marginTop="30dp"
android:text="@string/formulaire_combo"
android:textSize="20sp"/>
<CheckBox
android:id="@+id/formulaireCheckBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireCheckBox"
android:layout_marginLeft="100dp"
android:layout_toRightOf="@+id/textViewFormulaireCheckBox"
android:text="@string/formulaire_checkbox1"/>
<RadioGroup
android:id="@+id/formulaireRadioGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireRadioButton"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:orientation="horizontal">
<RadioButton
android:id="@+id/formulaireRadioButton1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radiobutton1"/>
<RadioButton
android:id="@+id/formulaireRadioButton2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radionbutton2"/>
<RadioButton
android:id="@+id/formulaireRadionButton3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radiobutton3"/>
</RadioGroup>
<SeekBar
android:id="@+id/formulaireSeekBar"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
android:layout_alignLeft="@+id/formulaireCheckBox1"/>
<EditText
android:id="@+id/formulaireEditText1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireEdtText"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:ems="10"
android:inputType="text">
</EditText>
<Switch
android:id="@+id/formulaireSwitch1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireBool"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:text="@string/formulaire_switch"
android:textOff="Non"
android:textOn="Oui"/>
<TimePicker
android:id="@+id/formulaireTimePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormulaireTime"
android:layout_alignLeft="@+id/formulaireEditTextMultiLignes"
android:timePickerMode="spinner"
/>
<EditText
android:id="@+id/formulaireEditTextMultiLignes"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_alignBaseline="@+id/textViewFormulaireMultilignes"
android:layout_alignBottom="@+id/textViewFormulaireMultilignes"
android:layout_marginLeft="50dp"
android:layout_toRightOf="@+id/textViewFormulaireMultilignes"
android:ems="10"
android:inputType="textMultiLine">
</EditText>
<Spinner
android:id="@+id/formulaireDropDownList"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_alignBottom="@+id/TextViewFormulaireCombo"
android:layout_alignLeft="@+id/formulaireEditTextMultiLignes">
</Spinner>
<DatePicker
android:id="@+id/formulaireDatePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormulaireDate"
android:layout_alignLeft="@+id/formulaireCheckBox1"
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/textViewFormulaireSeekBar"
android:layout_marginLeft="30dp"
android:layout_toRightOf="@+id/formulaireSeekBar"
android:text=""/>
</RelativeLayout>
表单的主要组件如下:
| |
| |
| |
| |
| |
| |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
|
1.18.3. 表单的字符串
表单的字符串在以下 [res/values/strings.xml] 文件中定义:
![]() |
<resources>
<string name="app_name">Exemple-17</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- vue 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="formulaire_checkbox">Cases à cocher</string>
<string name="formulaire_radioButton">Boutons Radio</string>
<string name="formulaire_seekBar">Seek Bar</string>
<string name="formulaire_saisie">Champ de saisie</string>
<string name="formulaire_bool">Booléen</string>
<string name="formulaire_date">Date</string>
<string name="formulaire_time">Heure</string>
<string name="formulaire_multilignes">Champ de saisie multilignes</string>
<string name="formulaire_listview">Liste</string>
<string name="formulaire_combo">Liste déroulante</string>
<string name="formulaire_checkbox1">1</string>
<string name="formulaire_checkbox2">2</string>
<string name="formulaire_radiobutton1">1</string>
<string name="formulaire_radionbutton2">2</string>
<string name="formulaire_radiobutton3">3</string>
<string name="formulaire_switch"></string>
<string name="formulaire_valider">Valider</string>
</resources>
1.18.4. 表单片段
![]() |
[View1Fragment] 类的定义如下:
package exemples.android.fragments;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.widget.*;
import android.widget.SeekBar.OnSeekBarChangeListener;
import exemples.android.R;
import exemples.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.vue1)
public class Vue1Fragment extends AbstractFragment {
// the fields of the view displayed by the fragment
@ViewById(R.id.formulaireDropDownList)
Spinner dropDownList;
@ViewById(R.id.formulaireButtonValider)
Button buttonValider;
@ViewById(R.id.formulaireCheckBox1)
CheckBox checkBox1;
@ViewById(R.id.formulaireRadioGroup)
RadioGroup radioGroup;
@ViewById(R.id.formulaireSeekBar)
SeekBar seekBar;
@ViewById(R.id.formulaireEditText1)
EditText saisie;
@ViewById(R.id.formulaireSwitch1)
Switch switch1;
@ViewById(R.id.formulaireDatePicker1)
DatePicker datePicker1;
@ViewById(R.id.formulaireTimePicker1)
TimePicker timePicker1;
@ViewById(R.id.formulaireEditTextMultiLignes)
EditText multiLignes;
@ViewById(R.id.formulaireRadioButton1)
RadioButton radioButton1;
@ViewById(R.id.formulaireRadioButton2)
RadioButton radioButton2;
@ViewById(R.id.formulaireRadionButton3)
RadioButton radioButton3;
@ViewById(R.id.textViewSeekBarValue)
TextView seekBarValue;
// drop-down list
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// check the first button
radioButton1.setChecked(true);
// the calendar
datePicker1.setCalendarViewShown(false);
// on seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// the drop-down list
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
}
@SuppressLint("DefaultLocale")
@Click(R.id.formulaireButtonValider)
protected void doValider() {
...
}
@Override
protected void updateFragment() {
// initialize drop-down list adapter
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
dropDownList.setAdapter(dataAdapter);
}
}
- 第 22–49 行:我们获取 XML 表单 [view1] 中所有组件的引用(第 18 行);
- 第 58 行:[setChecked] 方法允许您选中单选按钮或复选框;
- 第 60 行:默认情况下,[DatePicker] 组件会同时显示日期输入框和日历。第 60 行移除了日历;
- 第 62 行:[SeekBar].setMax() 设置滑块的最大值。最小值为 0;
- 第 63–74 行:我们处理滑块的事件。对于用户的每次操作,我们希望在第 49 行的 [TextView] 中显示滑块的当前值;
- 第 71 行:[progress] 参数代表滑块的当前值;
- 第 76–79 行:与下拉列表关联的 [String] 列表;
- 第 90 行:片段的 [updateFragment] 方法。当该方法被调用时,父类的 [activity] 变量已初始化;
- 第 92 行:数据源 [list] 已绑定至下拉列表适配器;
- 第 93–94 行:将 [dataAdapter] 绑定到下拉列表 [dropDownList];
- 第 84 行:[doValider] 方法与 [Valider] 按钮的点击事件相关联;
[doValider] 方法的目的是显示用户输入的值。其代码如下:
@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));
// on SeekBar
int progress = seekBar.getProgress();
messages.add(String.format("SeekBar [value=%d]", progress));
// the input field
String texte = String.valueOf(saisie.getText());
messages.add(String.format("Saisie simple [value=%s]", texte));
// the switch
boolean état = switch1.isChecked();
messages.add(String.format("Switch [value=%s]", état));
// the date
int an = datePicker1.getYear();
int mois = datePicker1.getMonth() + 1;
int jour = datePicker1.getDayOfMonth();
messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
// multi-line text
String lignes = String.valueOf(multiLignes.getText());
messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
// by the hour
int heure = timePicker1.getHour();
int minutes = timePicker1.getMinute();
messages.add(String.format("Heure [%d, %d]", heure, minutes));
// drop-down list
int position = dropDownList.getSelectedItemPosition();
String selectedItem = String.valueOf(dropDownList.getSelectedItem());
messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
// display
doAfficher(messages);
}
- 第 4 行:输入的值将被添加到消息列表中;
- 第 6 行:[CheckBox].isChecked() 方法用于判断复选框是否被选中;
- 第 9 行:[RadioGroup].getCheckedButtonId() 方法返回所选单选按钮的 ID,若未选中则返回 -1;
- 第 10 行:代码 [activity.findViewById(id)] 获取选中的单选按钮及其标签;
- 第 13 行:[SeekBar].getProgress() 方法返回滑块的当前位置;
- 第 19 行:[Switch].isChecked() 方法用于判断开关是处于开启(true)还是关闭(false)状态;
- 第 22 行:[DatePicker].getYear() 方法通过 [DatePicker] 对象获取所选年份;
- 第 23 行:[DatePicker].getMonth() 方法从 [DatePicker] 对象中返回所选月份,范围为 [0,11];
- 第 24 行:[DatePicker].getDayOfMonth() 方法通过 [DatePicker] 对象返回所选月份的日期,范围为 [1,31];
- 第 30 行:[TimePicker].getHour() 方法使用 [TimePicker] 对象返回所选的小时;
- 第 31 行:[TimePicker].getMinute() 方法使用 [TimePicker] 对象返回所选分钟;
- 第 34 行:[Spinner].getSelectedItemPosition() 方法返回下拉列表中选定项的位置;
- 第 35 行:[Spinner].getSelectedItem() 方法返回下拉列表中选中的项目;
用于显示已输入值列表的 [doAfficher] 方法如下:
private void doAfficher(List<String> messages) {
// we build the poster text
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// we display it
new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
- 第 1 行:该方法接收一个待显示的消息列表;
- 第 3–6 行:根据这些消息构建一个 [StringBuilder] 对象。对于字符串拼接,[StringBuilder] 类型比 [String] 类型更高效;
- 第 8 行:对话框显示第 3 行生成的文本:

1.18.5. 运行项目
运行项目并测试各种输入组件。
1.19. 示例-18:使用视图模式
1.19.1. 创建项目
我们通过复制 [示例-13] 项目来创建一个新项目 [示例-18]。
![]() | ![]() |
1.19.2. 视图模板
我们希望复用该项目中的两个视图,并将它们包含在模板中:
![]() |

这两个视图的结构将完全相同:
- 在 [1] 中,有一个页眉;
- 在 [2] 中,一个可能包含链接的左侧栏;
- 在 [3] 中,有一个页脚;
- 在 [4] 中,是内容。
这是通过修改活动的基类视图 [activity_main.xml] 来实现的;
![]() | ![]() |
[main]视图的XML代码如下:
<?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>
<exemples.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>
- 标题栏 [1] 由第 38–54 行生成;
- 左侧面板 [2] 由第 56–84 行生成;
- 页脚 [3] 由第 86–101 行生成;
- 正文 [4] 由第 78–84 行生成;
[main] XML 视图使用了 [res/values/colors.xml] 和 [res/values/strings.xml] 文件中的信息:
![]() |
[colors.xml] 文件内容如下:
<?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>
以及以下 [strings.xml] 文件:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">exemple-12</string>
<string name="action_settings">Settings</string>
<string name="titre_vue1">Vue n° 1</string>
<string name="textView_nom">Quel est votre nom :</string>
<string name="btn_Valider">Validez</string>
<string name="btn_vue2">Vue n° 2</string>
<string name="titre_vue2">Vue n° 2</string>
<string name="btn_vue1">Vue n° 1</string>
<string name="textView_bonjour">"Bonjour "</string>
<string name="txt_header">Header</string>
<string name="txt_left">Left</string>
<string name="txt_bottom">Bottom</string>
</resources>
为该项目创建运行时上下文并运行它。
1.20. 示例-19:[ListView] 组件
[ListView] 组件允许您针对列表中的每个项目重复显示特定的视图。该重复视图可以具有任意复杂度,从简单的字符串到允许您为列表中每个项目输入信息的视图均可。我们将创建以下 [ListView]:

列表中的每个视图包含三个组件:
- 一个用于显示信息的 [TextView];
- 一个 [CheckBox];
- 一个可点击的 [TextView];
1.20.1. 创建项目
我们通过克隆 [Example-18] 项目来创建一个新项目 [Example-19]。
![]() | ![]() |
![]() |
我们将按照[3]中的描述开展该项目。
1.20.2. 会议
![]() |
会话用于存储 Activity 与片段之间共享的数据:
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> liste=new ArrayList<>();
// getters and setters
...
}
- 第 11 行:两个视图共用的数据列表;
[Data] 类如下所示:
package exemples.android.architecture;
public class Data {
// data
private String texte;
private boolean isChecked;
// manufacturer
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
// getters and setters
...
}
- 第 6 行:将填充每个列表项第一个 [TextView] 的文本;
- 第 7 行:用于勾选或取消勾选列表中每个项目 [checkBox] 的布尔值;
1.20.3. [MainActivity]
[@AfterInject] 方法的代码如下:
// injection session
@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> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new Data("Texte n° " + i, false));
}
}
- 第 12–15 行:初始化会话中数据列表;
1.20.4. 初始 [View1] 视图
![]() | ![]() |
XML视图[view1.xml]显示了上方的区域[1]。其代码如下:
<?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_titre"
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/titre_vue1"
android:textSize="50sp" />
<Button
android:id="@+id/button_vue2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textView_titre"
android:layout_below="@+id/textView_titre"
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>
- 第 7–16 行:[TextView] 组件 [2];
- 第 27–35 行:[ListView] 组件 [4];
- 第 18–25 行:[Button] 组件 [3];
1.20.5. 由 [ListView] 重复显示的视图
![]() |
由 [ListView] 重复显示的视图是以下 [list_data] 视图:
<?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_Libellé"
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_Libellé"
android:layout_marginLeft="37dp"
android:layout_toRightOf="@+id/txt_Libellé"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/textViewRetirer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Libellé"
android:layout_alignBottom="@+id/txt_Libellé"
android:layout_marginLeft="68dp"
android:layout_toRightOf="@+id/checkBox1"
android:text="@string/txt_retirer"
android:textColor="@color/blue"
android:textSize="20sp" />
</RelativeLayout>
- 第 8–14 行:[TextView] 组件 [1];
- 第 16–23 行:[CheckBox] 组件 [2];
- 第 25–35 行:[TextView] 组件 [3];
1.20.6. [Vue1Fragment] 片段
![]() |
[Vue1Fragment] 片段负责管理 [vue1] XML 视图。其代码如下:
package exemples.android.fragments;
import android.view.View;
import android.widget.ListView;
import exemples.android.R;
import exemples.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;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// the fields of the view displayed by the fragment
@ViewById(R.id.listView1)
protected ListView listView;
// list adapter
private ListAdapter adapter;
// init done
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 doRetirer(int position) {
...
}
@Override
protected void updateFragment() {
if (!initDone) {
// associate data with [ListView]
adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
initDone = true;
}
// if the fragment has been (re)generated - in this case the ListView must be reconnected to its adapter
listView.setAdapter(adapter);
// if other fragments have changed the data source - in this case, refresh the ListView
adapter.notifyDataSetChanged();
}
}
- 第 15 行:XML 视图 [view1] 与该片段相关联;
- 第 26–30 行:[@AfterViews] 方法不执行任何操作。但是,必须将 [afterViewsDone] 变量设置为 true,因为父类 [AbstractFragment] 会使用它;
- 第 42–53 行:[updateFragment] 方法,该方法在片段每次变为可见时都会被调用。此处的实现假设片段可能脱离显示片段的邻接关系,从而重置其生命周期。虽然本例中并非如此,但如果应用程序包含 3 个片段且邻接关系为 1,则会发生这种情况;
- 第 44 行:[ListView] 适配器只需初始化一次;
- 第 46 行:我们将一个 [ListAdapter] 与该 [ListView] 关联。我们将构建这个类。它继承自 [ArrayAdapter] 类,我们之前已使用该类将数据与 [ListView] 关联。我们将各种信息传递给 [ListAdapter] 的构造函数:
- 当前活动的引用,
- 将为列表中每个项目实例化的视图标识符,
- 用于填充列表的数据源,
- 一个指向片段的引用。这将用于通过第 38 行中的 [doRemove] 方法处理 [ListView] 中 [Remove] 链接的点击事件;
- 第 50 行:适配器与 [ListView] 建立了绑定。同时,[lists] 数据源也与 [ListView] 建立了绑定。每次显示视图 #1 时,此处都会执行该操作。实际上,该操作仅需在 [@AfterViews] 方法执行后进行一次。此处的语句执行过于频繁。 我们需要一个布尔变量,用于标记 [@AfterViews] 方法刚刚执行完毕,因此 [ListView] 必须重新与适配器关联;
- 第 52 行:我们刷新了 [ListView]。在此示例中,此操作毫无意义,因为只有视图 #1 可以修改 [ListView] 的数据源。 让我们考虑一个更普遍的情况:视图 #2 也可能更改 [ListView] 的数据源。本文档后文将遇到此类示例。在这种情况下,从视图 #2 切换到视图 #1 时,视图 #1 中的 [ListView] 必须被刷新;
1.20.7. [ListView] 的 [ListAdapter]
![]() |
[ListAdapter] 类
- 配置 [ListView] 的数据源;
- 管理 [ListView] 中各种元素的显示;
- 处理这些元素的事件;
其代码如下:
package exemples.android.fragments;
import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {
// execution context
private Context context;
// the id of the layout displaying a line in the list
private int layoutResourceId;
// list data
private List<Data> data;
// the fragment that displays the [ListView]
private Vue1Fragment fragment;
// the adapter
final ListAdapter adapter = this;
// manufacturer
public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
super(context, layoutResourceId, data);
// memorize information
this.context = context;
this.layoutResourceId = layoutResourceId;
this.data = data;
this.fragment = fragment;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
}
- 第 5 行:[ListAdapter] 类继承自 [ArrayAdapter] 类;
- 第 19 行:构造函数;
- 第 20 行:别忘了使用前三个参数调用父类 [ArrayAdapter] 的构造函数;
- 第 22–25 行:我们存储构造函数的信息;
- 第 29 行:[ListView] 将反复调用 [getView] 方法来生成第 [position] 个元素的视图。返回的 [View] 结果是对所创建视图的引用。
[getView] 方法的代码如下:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// create the current ListView line
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// the text
TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
textView.setText(data.get(position).getTexte());
// the checkbox
CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
checkBox.setChecked(data.get(position).isChecked());
// the [Remove] link
TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
txtRetirer.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
fragment.doRetirer(position);
}
});
// manage the click on the checkbox
checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
data.get(position).setChecked(isChecked);
}
});
// we return the line
return row;
}
- 第 2 行:该方法接受三个参数。我们只使用第一个参数;
- 第 4 行:我们为第 [position] 个元素创建视图。这是 [list_data] 视图,其 ID 作为第二个参数传递给了构造函数。然后,我们获取刚刚实例化视图中各组件的引用;
- 第 6 行:获取 [TextView] #1 的引用;
- 第 7 行:我们将构造函数第三个参数传递的数据源中的文本赋值给它;
- 第 9 行:获取 [CheckBox] #2 的引用;
- 第 10 行:根据 [ListView] 数据源中的值将其选中或取消选中;
- 第 12 行:获取 [TextView] #3 的引用;
- 第 13–18 行:处理 [Remove] 链接的点击事件;
- 第 16 行:[Vue1Fragment] 的 doRetirer 方法处理此点击事件。让显示 [ListView] 的片段处理此事件更为合理,因为它拥有 [ListAdapter] 类所不具备的整体视图。[Vue1Fragment] 片段的引用作为第四个参数传递给了类构造函数;
- 第 20–25 行:处理复选框的点击事件。对其执行的操作会反映在显示的数据中。原因如下:[ListView] 是一个仅显示部分项的列表。因此,列表项有时被隐藏,有时被显示。 当需要显示第 i 个元素时,会调用上文第 2 行中针对第 i 个位置的 [getView] 方法。第 10 行将根据其关联的数据重新计算复选框的状态。因此,它必须随时间推移存储复选框的状态;
1.20.8. 从列表中移除项目
点击 [Remove] 链接的操作由 [Vue1Fragment] 片段中的以下 [doRetirer] 方法处理:
public void doRetirer(int position) {
// remove element n° [position] from the list
List<Data> liste = mainActivity.getListe();
liste.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 1st element fully visible or not
int firstPosition = listView.getFirstVisiblePosition();
// y offset of this element relative to the top of the ListView
// measures the height of any hidden part
View v = listView.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// refresh the [ListView]
adapter.notifyDataSetChanged();
// we position ourselves at the right spot on the ListView
listView.setSelectionFromTop(firstPosition, top);
}
- 第 1 行:获取 [ListView] 中被点击的 [Remove] 链接的位置;
- 第 3 行:获取数据列表;
- 第 4 行:删除位于 [position] 位置的项目;
- 第 15 行:刷新 [ListView]。如果不执行此操作,界面上将不会有任何变化。
- 第 5–13 行、第 17 行:一个相当复杂的过程。如果不执行这部分代码,将会发生以下情况:
- [ListView] 显示数据列表的第 15–18 行,
- 第16行被删除,
- 上方的第 15 行会将其完全重置,随后 [ListView] 便显示数据列表的第 0–3 行;
通过上述代码,删除操作得以执行,且 [ListView] 仍停留在被删除行之后的行上。
1.20.9. XML视图 [View2]
![]() | ![]() |
该视图的 XML 代码如下:
<?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_titre"
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/titre_vue2"
android:textSize="50sp" />
<Button
android:id="@+id/button_vue1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textViewResultats"
android:layout_marginTop="25dp"
android:layout_alignLeft="@+id/textView_titre"
android:text="@string/btn_vue1" />
<TextView
android:id="@+id/textViewResultats"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textView_titre"
android:layout_marginTop="50dp"
android:layout_alignLeft="@+id/textView_titre"
android:text="" />
</RelativeLayout>
- 第 6–15 行:[TextView] 组件 #1;
- 第 26–33 行:[TextView] 组件 #2;
- 第 17–24 行:[Button] 组件 #3;
1.20.10. [Vue2Fragment] 片段
![]() | 123 ![]() |
[Vue2Fragment] 片段负责管理 [vue2] XML 视图。其代码如下:
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import exemples.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 {
// fields of view
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@AfterViews
void initFragment(){
// memory
afterViewsDone=true;
}
@Click(R.id.button_vue1)
void navigateToView1() {
// navigate to view 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// displays list items selected in view 1
StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
for (Data data : mainActivity.getListe()) {
if (data.isChecked()) {
texte.append(String.format("(%s)", data.getTexte()));
}
}
texte.append("]");
txtResultats.setText(texte);
}
}
关键代码位于第 32 行的 [updateFragment] 方法中:
- 第 34 行:我们计算要在 [TextView] #2 中显示的文本;
- 第 35–39 行:遍历由 [ListView] 显示的数据列表。该列表存储在 Activity 中;
- 第 36 行:如果数据项 i 已被选中,则将其关联的标签添加到 [StringBuilder] 中;
- 第 41 行:[TextView] 显示计算出的文本;
1.20.11. 执行
为该项目创建一个运行配置并运行它。
1.20.12. 改进
在上一个示例中,我们使用了一个 List<Data> 数据源,其中 [Data] 类的定义如下:
package exemples.android.fragments;
public class Data {
// data
private String texte;
private boolean isChecked;
// manufacturer
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
...
}
在第 7 行中,我们使用了一个布尔变量来管理 [ListView] 中各项的复选框。通常,[ListView] 需要显示可以通过勾选复选框来选择的数据,即使数据源中的项目没有与该复选框对应的布尔字段。在这种情况下,您可以按以下步骤操作:
[Data] 类将变为如下形式:
package exemples.android.fragments;
public class Data {
// data
private String texte;
// manufacturer
public Data(String texte) {
this.texte = texte;
}
// getters and setters
...
}
我们创建一个从前一个类派生的 [CheckedData] 类:
package exemples.android.fragments;
public class CheckedData extends Data {
// checked item
private boolean isChecked;
// manufacturer
public CheckedData(String text, boolean isChecked) {
// parent
super(text);
// local
this.isChecked = isChecked;
}
// getters and setters
...
}
然后只需在代码中(MainActivity、ListAdapter、View1Fragment、View2Fragment)将 [Data] 类型替换为 [CheckedData] 类型。例如,在 [MainActivity] 中:
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// create a list of data
List<CheckedData> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new CheckedData("Texte n° " + i, false));
}
}
此版本的项目以 [Example-19B] 的名称提供。
1.21. 示例-20:使用菜单
1.21.1. 创建项目
我们将 [Example-19B] 项目复制为 [Example-20] 项目:
![]() | ![]() |
![]() | 3 ![]() |
我们将从视图 1 和 2 中移除按钮,并用菜单选项 [1-2] 替换它们。
1.21.2. 菜单的 XML 定义
![]() |
文件 [res/menu/menu_vue1] 定义了视图 #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/actionCacherMontrerTout"
android:title="@string/actionCacherMontrerTout"/>
<item
android:id="@+id/actionCacherMontrerActions"
android:title="@string/actionCacherMontrerActions"/>
<item
android:id="@+id/actionCacherMontrerActionsValider"
android:title="@string/actionCacherMontrerActionsValider"/>
</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/navigationVue2"
android:title="@string/navigationVue2"/>
</menu>
</item>
</menu>
菜单项由以下信息定义:
- android:id:元素的标识符;
- android:title:该项的标签;
- app:showsAsAction:指示该菜单项是否可放置在活动的操作栏中。[ifRoom] 表示如果操作栏有空间,则应将该项放置在操作栏中;
- 菜单选项本身可以是一个子菜单(<menu> 标签,第 25、29 行);
文件 [res / menu / menu_vue2] 定义了视图 #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/navigationVue1"
android:title="@string/navigationVue1"/>
</menu>
</item>
</menu>
1.21.3. 在抽象类 [AbstractFragment] 中管理菜单
我们将把菜单管理功能提取到这两个视图的父类 [AbstractFragment] 中:
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 daughter 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;
// manufacturer
public AbstractFragment() {
// init
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("création menu en cours"));
}
// retrieve # menu options if not already done
if (!initDone) {
// retrieve the # menu options
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// transfer the list of options to a table
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;
}
// the girl fragment is asked to stand
updateFragment();
}
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
...
}
// display menu options -----------------------------------
protected void setAllMenuOptions(boolean isVisible) {
....
}
protected void setMenuOptions(MenuItemState[] menuItemStates) {
...
}
// update girl class
protected abstract void updateFragment();
}
- 第 42 行:日志显示,每次显示片段时都会调用 [onCreateOptionsMenu] 方法。该方法被调用的时间非常晚,具体来说是在 [updateFragment] 方法被调用之后。这表明它可用于更新片段。这就是我们接下来要做的(第 63 行);
- 第 42 行:该方法有两个参数:
- [menu]:这是一个空菜单;
- [inflater]:一种工具,允许我们根据初始描述创建菜单。这里我们不会使用此选项,因为我们将使用 AA 注解来代为完成;
- 第 44 行:我们将菜单存储起来。稍后会用到它;
- 第 52–53 行:我们将所有菜单项的 ID 存储在第 28 行定义的数组中;
- 第 55–57 行:日志显示,当调用 [onCreateOptionsMenu] 方法时,[Fragment.getActivity()] 方法会返回与该片段关联的活动;
- 第 55 行:我们将该 Activity 作为 Android [Activity] 类的实例进行存储;
- 第 56 行:我们将该 Activity 作为 [IMainActivity] 接口的实例进行存储;
- 第 57 行:我们将会话存储起来;
- 第 59 行:我们注意到该类已经初始化,因此无需再次初始化(第 50 行);
- 第 63 行:我们要求子片段更新自身。之所以能这样做,是因为该片段既处于可见状态,又与其视图和菜单相关联;
用于获取菜单项 ID 的 [getMenuOptions] 方法如下:
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// scroll through all menu items
for (int i = 0; i < menu.size(); i++) {
// item n° i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// if item n° i is a sub-menu, then start again
if (menuItem.hasSubMenu()) {
// recursivity
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
[setAllMenuOptions] 方法允许您隐藏或显示所有菜单选项;
protected void setAllMenuOptions(boolean isVisible) {
// update all menu options
for (int menuItemId : menuOptions) {
menu.findItem(menuItemId).setVisible(isVisible);
}
}
[setMenuOptions] 方法允许您隐藏或显示某些菜单选项;
protected void setMenuOptions(MenuItemState[] menuItemStates) {
// update certain menu options
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
[MenuItemState] 类的定义如下:
![]() |
package exemples.android.architecture;
public class MenuItemState {
// menu option identify
private int menuItemId;
// option visibility
private boolean isVisible;
// manufacturers
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// getters and setters
...
}
1.21.4. [View1Fragment] 片段中的菜单管理
[View1Fragment] 类变为如下形式:
@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 valider() {
// a message is displayed
Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
}
private boolean actionCacherMontrerTout = true;
@OptionsItem(R.id.actionCacherMontrerTout)
void cacherMontrerTout() {
// we change state
actionCacherMontrerTout = !actionCacherMontrerTout;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuNavigation, actionCacherMontrerTout), new MenuItemState(R.id.menuActions, actionCacherMontrerTout)});
}
private boolean actionCacherMontrerActions = true;
@OptionsItem(R.id.actionCacherMontrerActions)
void actionCacherMontrerActions() {
// we change state
actionCacherMontrerActions = !actionCacherMontrerActions;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
}
private boolean actionCacherMontrerActionsValider = true;
@OptionsItem(R.id.actionCacherMontrerActionsValider)
void actionCacherMontrerActionsValider() {
// we change state
actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
}
...
@Override
protected void updateFragment() {
....
// update the menu
//setMenuOptions(...)
}
}
- 第 2 行:菜单 [res/menu/menu_vue1.xml] 与片段相关联;
- 第 48 行:当 [updateFragment] 方法被调用时,菜单也可随之更新以反映片段的新状态;
- 第 7 行:注解 [@OptionsItem(R.id.navigationVue2)] 标注了在点击菜单选项 [Navigation / View 2] 时必须执行的方法;
- 第 19–25 行:要隐藏菜单的某个分支,只需隐藏其根选项即可;
- 第 24 行:显示或隐藏根选项 [menuNavigation, menuActions];
- 第 40 行:要在菜单分支中显示一个选项,不仅需要显示该选项,还需显示从叶子选项向上移动至菜单根节点时遇到的所有选项;
1.21.5. [Vue2Fragment] 片段中的菜单管理
在视图 2 的片段中可以找到类似的代码:
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.models.CheckedData;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue2)
@OptionsMenu(R.menu.menu_vue2)
public class Vue2Fragment extends AbstractFragment {
// fields of view
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@OptionsItem(R.id.navigationVue1)
void navigateToView1() {
// navigate to view 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// displays list items selected in view 1
StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
for (CheckedData data : session.getListe()) {
if (data.isChecked()) {
texte.append(String.format("(%s)", data.getTexte()));
}
}
texte.append("]");
txtResultats.setText(texte);
// update the menu
// setMenuOptions(...)
}
}
- 第 35 行:显示 [导航 / 视图 1] 选项;
- 第 17-20 行:当点击 [导航 / 视图 1] 选项时,调用 [navigateToView1] 方法;
1.21.6. 执行
为该项目创建运行时上下文并运行。
1.22. 示例-21:重构 [AbstractFragment] 类
前一个示例向我们展示了,当片段拥有菜单时,其 [onCreateOptionsMenu] 方法是请求片段进行自我更新的理想位置:
- 该方法在片段即将显示时会被调用一次;
- 调用时,片段与其所属的 Activity、视图及菜单之间的关联已建立;
为演示这一点,我们将重温示例 12,该示例包含多个可调整排列顺序的片段。在该示例中,这些片段没有菜单。我们将为它们关联一个空菜单。
1.22.1. 创建项目
我们将 [Example-12] 项目复制为 [Example-21] 项目:
![]() | ![]() |
1.22.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="exemples.android.MainActivity">
</menu>
这里需要理解的是,该 Activity 已经拥有自己的菜单 [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="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
当一个 Activity 已有菜单时,与片段关联的菜单会被添加到该 Activity 的菜单中:因此您将同时拥有两个菜单的选项。在此情况下,片段的菜单将为空。因此您只会看到 Activity 的菜单。
1.22.3. 片段
![]() |
我们复用前一个示例中的抽象类 [AbstractFragment](参见第 1.21.3 节)。我们将菜单 [menu_fragment] 与这两个片段关联起来:
@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 Vue1Fragment extends AbstractFragment {
在 [PlaceholderFragment] 和 [Vue1Fragment] 这两个片段中,我们移除了对旧的抽象类 [AbstractFragment] 的所有引用。
1.22.4. 执行
运行应用程序并验证其是否正常工作。检查日志以查看 [AbstractFragment] 类的 [onCreateOptionsMenu] 方法何时被调用。现在,正是该方法调用了子片段的 [updateFragment] 方法。
1.23. 示例-22:保存/恢复 Activity 和片段的状态
1.23.1. 问题
在此我们将解决 Android 设备旋转(纵向 <--> 横向)的问题。为说明这一点,我们将重温之前的示例 21:

如果旋转设备 [1],我们会看到以下新的视图:

我们可以看到:
- 在 [1] 中,[Fragment #3] 标签页已消失;
- 在 [2] 中,显示的文本确实是第 3 号片段的内容,但访问计数器不正确;
在此轮转过程中,日志如下:
- 第 1 行:我们可以看到该 Activity 已被完全重建;
- 第 3–7 行:此情况同样适用于该 Activity 管理的五个片段;
- 第 21 行:片段 #3 即将显示。我们可以看到,在递增之前,访问计数为 0;
因此,我们可以对旋转后的结果进行如下解释:
- [MainActivity] 类最初创建了一个标签栏,其中仅有一个标有 [View 1] 的标签。这是当前可见的标签;
- 设备旋转后,页面管理器 [mViewPager] 会重新显示同一个片段,即片段 #3。这里需要特别注意的是,标签页和片段是不同的概念,且具有不同的生命周期。片段 #3 的 [updateFragment] 方法将被执行:
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// increment visit no
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- 第 7 行:从会话中读取了上次访问的 ID。然而,会话——和其他所有内容一样——已被重置,访问 ID 也被重置为零。这解释了片段 #3 中显示的结果;
1.23.2. 保存/恢复 Activity 和 Fragment 的方法
1.23.2.1. 解决方案 1:手动备份
当设备旋转时,会调用该 Activity 的两个方法:
// backup / restore management ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// parent
super.onSaveInstanceState(outState);
// backup activity status
// ....
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// restoring activity
// ...
}
- 第 2–8 行:系统在旋转设备时会调用 [onSaveInstanceState] 方法。此时可以保存 Activity 的状态。如果不进行任何操作,则不会保存任何内容。 必须将 Activity 的状态保存在传递给该方法的 [Bundle outState] 参数中。[Bundle] 类类似于字典。它提供了 [putString, putInt, putLong, putBoolean, putChar, ...] 等方法,这些方法的签名均为:void putT(String key, T value);
- 第 10–16 行:当 Activity 被创建时,会调用 [onCreate] 方法。如果 Activity 的状态已被保存,则该保存的状态会通过 [Bundle savedInstanceState] 参数传递给它。 要检索已保存的值,可以使用单参数方法,如 [getString, getInt, getLong, getBoolean, getChar, ...]:T getT(String key);
片段(Fragment)也拥有这两个相同的方法来保存其状态。
我们将利用这些信息来保存和恢复示例 21 的状态。为此,我们将 [Example-21] 项目复制为 [Example-22]。
1.23.2.2. 解决方案 2:自动保存
Android 文档指出,当设备旋转时,可以通过使用 [Fragment].setRetainInstance(true) 语句来防止片段被销毁。StackOverflow 上的一些文章建议仅对没有视觉界面的片段使用此指令 [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]。 我通过两个示例对这一说法进行了测试:示例-17(第 1.18 节——一个显示表单的单片段应用)和示例-21(第 1.22 节——一个五片段应用)。在这两种情况下,将这一条指令应用于应用的所有片段,都不足以在设备旋转时正确恢复显示的视图。 与其构建两个模型(一个基于 [setRetainInstance(true)],另一个基于 [setRetainInstance(false)]——即默认值),我决定遵循 [StackOverflow] 的建议,将 [setRetainInstance(boolean)] 方法的默认值保留为 false。 在本文档的其余部分中,从未使用过语句:[Fragment].setRetainInstance(true)。
1.23.3. [Example-22] 项目的备份/恢复方法
[Example-22] 项目的演变过程如下:
![]() |
出现了两个新类:
- [PlaceHolderFragmentState],用于存储类型为 [PlaceHolderFragment] 的片段的状态;
- [Vue1FragmentState],用于存储 [Vue1Fragment] 类型片段的状态;
这些类如下所示:
package exemples.android;
public class Vue1FragmentState {
// status Vue1Fragment
private boolean hasBeenVisited=false;
// getters and setters
...
}
- 第 5 行:如果片段 [Vue1Fragment] 已被访问(显示)至少一次,则布尔值 [hasBeenVisited] 为 true。该字段是为示例而创建的,因为片段 [Vue1Fragment] 没有任何需要保存的内容;
[PlaceHolderFragmentState] 类的定义如下:
package exemples.android;
public class PlaceHolderFragmentState {
// whether visited or not
private boolean hasBeenVisited;
// display text
private String text;
// getters and setters
...
}
- 第 5 行:我们可以看到布尔变量 [hasBeenVisited];
- 第7行:片段在需要保存时显示的文本。我们看到,在旋转过程中,该文本丢失了;
片段的状态将存储在会话中,而活动将负责保存和恢复该会话。会话的演变过程如下:
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;
// n° fragment type [PlaceholderFragment] displayed in the second tab
private int numFragment = -1;
// selected tab no
private int selectedTab = 0;
// n° current view
private int currentView;
// fragment backups ---------------
private Vue1FragmentState vue1FragmentState;
private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];
// manufacturer
public Session() {
for (int i = 0; i < placeHolderFragmentStates.length; i++) {
placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
}
vue1FragmentState = new Vue1FragmentState();
}
// getters and setters
...
}
- 第 18 行:[Vue1Fragment] 片段的状态;
- 第 19 行:[PlaceHolderFragment] 类型片段的状态;
- 第 22–27 行:在会话构造函数中,初始化第 18 和 19 行中的字段;
- 第 12–15 行:出现了两个新字段:
- 第 13 行:最后选中标签页的编号;
- 第 15 行:最后显示的片段编号;
该活动按以下方式保存/恢复会话:
// backup / 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) {
// session recovery
try {
session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
});
} catch (IOException e) {
e.printStackTrace();
}
// log
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
}
- 第 8 行:会话被保存为 JSON 字符串;
- 第 29 行:从 JSON 字符串中恢复会话;
为了管理片段的保存和恢复,抽象类 [AbstractFragment] 演变如下:
// backup / restore management -----------------------------------------------
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// backup?
if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
// the fragment will 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 fragment only if visible
if (isVisibleToUser && !saveFragmentDone) {
saveFragment();
saveFragmentDone = true;
}
}
// girls' classes
protected abstract void updateFragment();
protected abstract void saveFragment();
- 我们决定在两个时间点将片段的状态保存到会话中:
- 第 2–14 行:当片段状态从可见变为隐藏时;
- 第 29–42 行:当系统指示应保存片段且片段处于可见状态时(第 38 行);
此机制可避免不必要的频繁保存。事实上,由于我们在片段 i 从可见变为隐藏时已保存其状态,因此当片段 j 显示且发生旋转时,无需再次保存片段 i。如果自上次保存以来它未被重新显示,则其状态未发生变化。 此时只需保存片段 j 的状态。该机制还有另一个优势:不仅在设备旋转时需要保存片段状态。 还存在片段之间的纯导航情况,例如在标签页系统中。在这种情况下,我们希望以片段上次显示时的状态将其检索回来。如果该片段曾在某个时刻被移出显示片段的邻近区域,其状态可能会部分丢失。此时片段不会被完全重建,但与其关联的视图会被重建。片段变为隐藏时执行的保存操作将用于恢复该视图的最后状态;
- 第 10、40 行:为避免连续执行两次保存操作,使用布尔变量 [saveFragmentDone] 来标记保存操作已完成;
- 第 9、39 行:要求子片段保存其状态。[saveFragment] 方法是抽象的(第 47 行)。因此,由子类负责实现该方法;
- 第 16–26 行:使用 [onActivityCreated] 方法将布尔值 [fragmentHasToBeInitialized] 设置为 true。这是因为子片段需要知道,它必须根据在会话中找到的状态,对片段的状态进行完全重新初始化;
仍在 [AbstractFragment] 类中,[onCreateOptionsMenu] 方法的更改如下:
// fragment update
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// memory
this.menu = menu;
// log
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
...
// the girl fragment is asked to update itself
updateFragment();
// backup to do
saveFragmentDone = false;
}
- 第 14 行:我们看到,在执行保存操作时,布尔变量 [saveFragmentDone] 被设置为 true。 在某个时刻,它必须被重置为 false。当子片段的 [updateFragment] 方法(第 12 行)被执行时,该片段会变为可见。然而,片段必须在可见时被保存,具体来说是在它从可见状态过渡到隐藏状态的瞬间。然后我们将布尔变量 [saveFragmentDone] 设为 false,以便执行保存操作;
1.23.4. 保存 [Vue1Fragment] 片段
片段是在父类 [AbstractFragment] 调用的 [saveFragment] 方法中进行保存的:
// save fragment status
@Override
public void saveFragment() {
// log
if (isDebugEnabled) {
Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
}
// in-session saving of fragment status
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();
}
}
}
- 第 9–11 行:将片段的状态保存到会话中。调用 [saveFragment] 方法时,该片段处于可见状态。因此,布尔值 [hasBeenVisited] 必须设置为 true(第 10 行);
1.23.5. 保存 [PlaceHolderFragment] 片段
片段是在父类 [AbstractFragment] 调用的 [saveFragment] 方法中保存的:
@Override
public void saveFragment() {
// save fragment state in 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();
}
}
}
- 第 4–7 行:将片段的状态保存到会话中;
- 第 5 行:保存 [TextView] textViewInfo 当前显示的文本;
- 第 6 行:将片段的 [hasBeenVisited] 布尔值设置为 true;
- 第 7 行:将片段的状态保存在会话的 [placeHolderFragmentStates] 数组中。待初始化的元素索引为片段的章节编号减一;
1.23.6. 恢复 [Vue1Fragment] 片段
片段在 [updateFragment] 方法中进行恢复:
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
}
// restoration?
if (fragmentHasToBeInitialized) {
// restoration condition
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 no
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// the visit number is displayed
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
// change n° current view
session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
}
- 第 8–12 行:恢复片段的状态。布尔值 [fragmentHasToBeInitialized] 由父类 [AbstractFragment] 初始化。当其值为 true 时,表示片段刚刚被重建,必须重新初始化。此处即执行该操作。在此具体示例中,无需执行任何操作。 我们只是演示了如何从片段的保存状态中获取布尔值 [hasBeenVisited](第 10 行);
- 第 11 行:别忘了将 [fragmentHasToBeInitialized] 重新设为 false,这样当我们在设备未旋转的情况下返回此片段时,就不会对片段进行不必要的初始化;
- 第 18–26 行:递增访问计数器。这里存在一个挑战:在恢复片段时,我们不希望递增此计数器。我们需要在此区分以下两种情况:
- 将用户带回 [View 1] 标签页的简单导航;
- 用户在显示 [View 1] 标签页时旋转设备所导致的恢复;
我们通过会话中存储的视图编号来区分这两种情况。该编号即最后显示的视图编号(第28行)。
- 第 18 行:如果最后显示的视图编号与当前视图编号不同,则执行导航操作而非刷新;
- 第 21–25 行:递增访问计数器并显示其值;
1.23.7. 恢复 [PlaceHolderFragment]
在 [updateFragment] 方法中恢复片段:
// 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 it?
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 no
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
newText = String.format("%s, visite %s", text, numVisit);
} else {
// we are dealing with a restoration
PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
newText = state.getText();
}
// text display
textViewInfo.setText(newText);
// current view
session.setCurrentView(numView);
}
- 第 15-16 行:确定正在更新的视图编号;
- 第 18-22 行:处理设备方向改变后片段处于保存/恢复循环的情况。必须在此处进行恢复。这通常涉及恢复片段的某些字段;
- 第 20 行:第 2 行中的 [text] 字段必须包含片段显示的初始文本:[Hello world from section i]。此处必须重新生成该文本;
- 第 21 行:注意片段已初始化;
- 第 24–36 行:与之前的 [Vue1Fragment] 片段一样,在恢复过程中不得递增访问计数器。与之前一样,我们必须区分导航和恢复;
- 第 32–36 行:恢复情况;
- 第 34 行:从会话中检索设备旋转前的片段状态;
- 第 35 行:检索当时显示的文本;
- 第 38 行:再次显示该文本;
- 第 40 行:将新显示视图的编号记录在会话中;
1.23.8. 标签页管理
前面的章节并未涉及标签页管理。然而,在示例 21 中旋转设备时,我们遇到了一个问题:只有第一个标签页 [视图 1] 被保留,第二个标签页丢失了。
我们在 [MainActivity] 类中通过以下方式解决了此问题:
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "afterViews");
}
// toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
...
// 1st tab
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// 2nd tab ?
int numFragment = session.getNumFragment();
if (numFragment != -1) {
TabLayout.Tab tab2 = tabLayout.newTab();
tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
tabLayout.addTab(tab2);
}
// which tab to select?
tabLayout.getTabAt(session.getSelectedTab()).select();
...
}
- 第 14–16 行:创建第一个标签页;
- 第18–23行:创建第二个标签页。为确定是否创建,我们检查会话中标签页2所显示片段的编号。如果该编号不为-1(其初始值),则创建第二个标签页。此时,我们拥有两个标签页,其中第一个默认被选中;
- 第26行:从会话中获取保存/恢复前选中的标签页编号,并重新选中该标签页。如果代码尚未初始化[selectedTab]字段,则使用其初始值0;











































































































































































































































































































































