Skip to content

1. Android编程入门

该文档的PDF版本可在此处获取 |HERE|。

文档中的示例可通过 |HERE| 获取。

1.1. Introduction

1.1.1. 目录

本文档是对以下现有文档的改写:

  1. 《Android开发者指南》J2EE
  2. 通过实例了解 Android 平板电脑编程
  3. 使用 Android 平板电脑控制 Arduino
  4. 《通过实例学习 Android 平板编程入门》第 2 版

并引入了以下新内容:

  • 文档 1 介绍了一种名为 AVAT(活动-视图-操作-任务)的架构,用于简化 Android 应用中的异步编程。该文档使用标准库 RxJava 来管理异步操作;
  • 文档2使用的是IDE Eclipse搭配Android插件。本文档则使用Android Studio;
  • 文档3保持原样;
  • 文档4使用了[Android Annotations](AA)库,配合IDE IntelliJ IDEA社区版。 本文档完整收录了文档4的内容,但存在以下差异:
    • IDE 现已更名为 Android Studio;
    • 所有客户端或服务器端项目均采用 Gradle 构建系统(文档 4 中有时使用 Maven);
    • 异步编程采用 RxJava 库(文档 4 中使用的是 AA 库);
  • 本文探讨了先前文档中未涉及或仅略有涉及的领域:
    • 片段邻接性概念;
    • 活动及其片段的保存/恢复;
    • 片段的生命周期;

最后,本文介绍了与Web服务/jSON通信的Android客户端框架,其中提取了此类客户端中经常出现的大量元素。从第2章开始的所有示例均采用了该框架。这是本文真正具有创新性的部分。

以下示例将予以介绍:

Exemple
性质
1
导入现有 Android 项目
2
一个基本的 Android 项目
3
一个基本的 [Android Annotations] 项目
4
视图与事件
5
视图间的导航
6
标签页导航
7
在 Gradle 中使用 [Android Annotations] 库
8 à 12
Android 应用片段管理
13
视图导航的重新设计
14
两层架构
15
客户端/服务器架构
16
使用 RxJava 管理异步处理
17, 17B
数据录入组件
18
使用视图模板
19
ListView 组件
20
使用菜单
21
为片段使用父类
22, 22B
保存和恢复活动及片段的状态
23
天气客户端
Chap 2
与 Web 服务通信的 Android 客户端框架 / jSON。其中提取了此类 Android 客户端中经常出现的大量元素。
Chap 3
医疗机构预约管理
Chap 4
应用练习——基础薪资管理
Chap 5
应用练习——Arduino控制卡

本文件曾用于昂热大学IstiA工程学院[istia.univ-angers.fr]的应届毕业生课程。这解释了文本中偶尔出现的独特语气。 这两项应用练习均摘自TP,本文仅提供解决方案的概要,具体实现需由读者自行完成。

示例的源代码可在此处获取 |ICI|。要运行这些示例,您需按照第6.12节中的步骤操作。

本文档是关于Android编程的入门指南,并非详尽无遗,主要面向初学者。

Android 编程的参考网站位于 URL [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 节);
  • IDE [Android Studio](参见第 6.11 节);

1.2. 示例-01:导入一个 Android 示例

1.2.1. 创建项目

让我们使用 Android Studio 创建第一个 Android 项目。首先创建一个空文件夹 [exemples],用于存放所有项目:

  

然后使用 Android Studio 创建一个项目。我们将首先导入 IDE [1-5] 随附的示例之一:

 

Image

项目导入可能会因项目创建时使用的环境与当前执行环境不匹配而导致错误。这为我们提供了一个解决此类错误的机会。在此,我们遇到以下错误:

导入的项目由以下文件进行配置:[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"
}

// 示例构建使用多个目录来
// 将模板代码和通用代码与
// 主示例代码。
List<String> dirs = [
    'main',     // 主示例代码;有意思的内容请看这里。
    'common',   // 被多个示例重复使用的组件
    'template'] // 由示例模板生成器生成的模板代码

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-entrée]:

 

此外,还可能出现关于 Gradle 版本的错误:

 

该错误源于项目文件 [build.gradle] 要求的 Gradle 版本(如下文第 6 行所示的 2.10)与实际版本不匹配:


buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}

与 [<projet>/gradle/wrapper/gradle-wrapper.properties] 文件中记录的版本不一致:


#2013年4月10日 星期三 15:27:10 PDT
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。

要访问文件 [<projet>/gradle/wrapper/gradle-wrapper.properties],请使用该项目的项目视图:

修正上述问题后,即可编译应用程序 [1],启动 Genymotion 模拟器 [2],然后运行项目 [3]:

 

Image

停止应用程序:

  

现在可以关闭该项目了。我们将创建一个新项目。

  

1.2.2. 关于IDE的几点说明

1.2.2.1. 视图

IDE Android Studio(AS)提供了多种视图来处理项目。我们将主要使用其中两种:

  • [Android] [1] 视图:
  • [Project] [4] 视图;
 
  

大多数情况下,我们将使用 [Android] 视图。当我们将一个项目复制到另一个项目中时,我们需要使用 [Project] 视图。

1.2.2.2. 运行管理

有多种方法可以执行/停止/重新执行 AS 项目。首先是工具栏上的按钮:

按钮 [Rerun] [3],用于停止项目 [2] 的执行,然后重新启动 [1]。

1.2.2.3. 缓存管理

Android Studio 会维护其管理的项目缓存,以使 IDE 尽可能响应迅速。在 Android 2.1 版本(2016 年 5 月)中,该缓存经常无法反映刚刚进行的代码修改。在这种情况下,需要清空该缓存:

在 Android 2.1(2016年5月)版本中,上述操作往往需要重复多次,有时甚至仍无法解决检测到的异常。解决方案是禁用 [Instant Run] 技术:

  • 在[3-4]中,所有功能均已禁用;

此后,我们一直使用该缓存配置进行操作,未再遇到任何问题。

1.2.2.4. 日志管理

在项目运行时,Android 监控器中会显示日志:

在 [Android Monitor] [1] 选项卡中,日志会显示在 [logcat] [2] 选项卡中。 [3] 按钮用于清除日志。当需要查看特定操作的日志时,此按钮非常有用:

  • 清除日志;
  • 在 Android 设备上执行需要查看日志的操作;
  • 此时显示的日志即与该操作相关的日志;

[4] 支持多种日志级别。默认情况下,系统会选择 [Verbose] 模式。这意味着将显示所有级别的日志。通过 [4],可以选择特定的日志级别。

日志对于了解项目执行过程中某些方法在何时被调用非常有用。我们经常会用到它们。以项目 [Exemple-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, ligne 14]、[onCreateOptionsMenu, ligne 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] 级别的日志。其第一个参数是日志消息的来源。实际上,不同来源可能会向日志控制台发送消息。为了区分它们,我们使用这个第一个参数。第二个参数是要写入日志控制台的消息;

如果我们再次运行项目 [Exemple-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-Supp],并删除所有现有的 Virtualbox 任务:

完成上述操作后,请通过 Android Studio 重新启动 Genymotion 模拟器。

1.2.2.6. 已创建二进制文件 APK

项目编译后生成后缀为 .apk 的二进制文件:

共有两个版本:一个名为 [debug],另一个名为 [debug-unaligned]。应使用前者,后者为中间版本。 在 [4] 中生成的 .pak 二进制文件可直接传输到模拟器或 Android 设备上。要将其传输到模拟器,只需用鼠标将其拖拽到模拟器上即可。

1.3. 示例-02:一个基础的 Android 项目

使用 Android Studio 创建一个新的 Android 项目 [1-12]:

 

在 [13] 中,我们运行该应用程序。随后,Genymotion 模拟器上显示出 [14] 的界面。

1.3.1. 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 行:活动名称;

这两项信息来源于创建项目时的输入:

  • 清单文件(package)的第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。此处生成了一个名为 [1] 的 Activity:[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]。未来所有活动都将遵循这一模式;
  • 第 9 行:当 Activity 创建时,将执行 [onCreate] 方法。这发生在显示与该 Activity 关联的视图之前;
  • 第 10 行:调用父类的 [onCreate] 方法。必须始终执行此操作;
  • 第 11 行:文件 [activity_main.xml] [2] 是与该活动关联的视图。该视图的定义如下:

<?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] 中字符串 [app_name] 的标识符:
  • 第 19 行:属性 [R.mipmap.ic_launcher] 是图像 [res / mipmap / ic_launcher] 的标识符;

因此需注意,当在代码中引用 [R.layout.activity_main] 时,实际上是在引用 [R] 类的某个属性。IDE 帮助我们了解该类的各个元素:

1.3.4. 应用程序的运行

要运行一个 Android 应用程序,我们需要创建一个运行配置:

  • 在 [1] 中,选择 [Edit Configurations];
  • 该项目最初创建时使用了配置 [app],我们将删除 [2] 并重新创建;
  • 在 [3] 中,创建一个新的运行配置;
  
  • 在 [4] 中,选择 [Android Application];

Image

  • 在 [5] 中,从下拉列表中选择模块 [app];
  • 在 [6-8] 中,保留默认值;
  • 在 [7] 中,默认活动为 [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] 中,选择 [Show Chooser Dialog],该选项允许选择应用程序的运行设备(模拟器、平板电脑);
  • 在 [9] 中,指定需将此选择保存;
  • 确认配置;
  
  • 在 [11] 中,启动 [Genymotion] 模拟器管理器(参见第 6.9 节);
  • 在 [12] 中,选择一个平板电脑模拟器并启动 [13];
  • 在 [14] 中,执行运行配置 [app];
  • 在 [15] 中会显示运行设备选择表单。此处仅有一个可用选项:之前启动的 [Genymotion] 模拟器;

软件模拟器稍后将显示以下视图:

Image

1.3.5. 活动的生命周期

让我们回顾一下 [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] 方法属于可在活动生命周期中调用的方法之一。Android 文档列出了这些方法:

 
  • [1]:在活动启动时会调用方法 [onCreate]。该方法用于将活动与视图关联,并获取视图中各组件的引用;
  • [2-3]:随后调用 [onStart, onResume] 方法。 可以看出,[onResume]方法是在进入当前正在执行的活动状态[4]之前最后执行的方法;

1.4. 示例-03:使用库 [Android Annotations] 重写项目 [Exemple-02]

接下来我们将引入 [Android Annotations] 库,该库可简化 Android 应用程序的编写。为此,我们将按照 [1-16] 中的步骤,将示例 [Exemple-02] 复制为 [Exemple-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>

现在,我们运行新应用程序,该应用程序已继承了 [Exemple-02] 的全部配置:

 

在 [19] 中,我们得到了与 [Exemple-02] 相同的结果,但名称有所不同。

现在我们将引入库 [Android Annotations],为方便起见,我们将它称为 AA。 该库引入了用于标注 Android 源代码的新类。这些标注将被处理器使用,该处理器将在模块中生成新的 Java 类,这些类将与开发者编写的类一样参与该模块的编译。因此,我们得到以下编译链:

首先,我们将在文件 [build.gradle] 中添加对注解编译器 AA(即上述处理器)的依赖:


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 {
    // 自 Android 的 Gradle 插件 0.11 起,您必须使用 android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'
  • 第 8 行:将在 Maven 中央仓库(第 3 行)中查找的 [android-apt] 插件版本;
  • 第 13 行:启用该插件;

此时,请确认 [app] 运行配置是否仍能正常工作。

现在,我们将在类 [MainActivity] 中引入 AA 库的第一个注解:

  

目前 [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行)的子类。其参数是与该活动关联的视图;

该注解将生成一个从类 [MainActivity] 派生的类 [MainActivity_],而该类才是真正的活动。因此,我们需要按以下方式修改项目 [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 行:新业务;

完成上述操作后,我们可以编译项目 [1]:

 
  • 在 [2] 中,可以看到生成的类 [MainActivity_] 位于文件夹 [app / build / generated / source / apt / debug] 中;

生成的类[MainActivity_]如下:


//
// DO NOT EDIT THIS FILE。
// 使用 AndroidAnnotations 4.0.0 生成。
// 
// 您可以创建包含此文件的大型作品,并根据您选择的条款分发该作品。
//


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节中描述的步骤,将[Exemple-02]复制为[Exemple-03]:

我们:

  • 将项目 [Exemple-03] 复制为 [Exemple-04](此前已从 [Exemple-03] 中删除了 [app / build] 文件夹);
  • 加载项目 [Exemple-04];
  • 在文件 [app / res / values / strings.xml] 中更改项目名称(Android 视图);
  • 删除文件 [Exemple-04 / Exemple-04.iml](项目视图);
  • 编译并运行该项目;
 

1.5.2. 构建视图

现在,我们将使用图形编辑器修改项目 [Exemple-04] 显示的视图:

  • 在 [1-4] 中,创建一个新视图 XML;
  • 在 [5] 中,为视图命名;
  • 在 [6] 中,指定视图的根标签。 此处,我们选择一个容器 [RelativeLayout]。在此组件容器中,组件之间的位置关系为:“位于右侧”、“位于左侧”、“位于下方”、“位于上方”;
  

生成的文件 [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">

</RelativeLayout>
  • 第 2 行:一个空的 [RelativeLayout] 容器,它将占据平板电脑的全部宽度(第 3 行)和全部高度(第 4 行);
  • 在 [1] 中,在显示的 [vue1.xml] 视图中选择 [Design] 选项卡;
  • 在 [2-4] 中,切换至平板模式;
  • 在 [5] 中,将平板电脑缩放比例设为 1;
  • 在 [6] 中,为平板电脑选择“横向”模式;
  • 屏幕截图 [7] 总结了所做的选择。
  • 在 [1] 中,选中 [Large Text] 并将其拖拽到 [2] 视图上;
  • 在 [3] 中,双击该组件;
  • 在 [4] 中,修改显示的文本。与其将其“硬编码”在 XML 视图中,我们将把它外部化到 [res / values / string.xml] 文件中
  • 在 [5] 中,向文件 [strings.xml] 中添加一个新值;
  • 在 [8] 中,为该字符串分配一个标识符;
  • 在 [9] 中,给出了字符串的值;
  • 在 [10] 中,显示上一步骤验证通过后的新视图;
  • 双击组件后,修改其标识符为 [11];
  • 改为 [12],在组件属性中,将字符大小从 [50sp];
  • 改为 [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>
  • 图形界面中的修改位于第10、11和14行。[TextView]的其他属性要么是默认值,要么由组件在视图中的位置决定;
  • 第 7-8 行:组件的高度与宽度取决于其所包含文本(wrap_content)的尺寸;
  • 第13行:组件顶部与视图顶部对齐(第13行),并向下偏移50像素(第13行);
  • 第12行:组件的左侧与视图左侧对齐(第13行),向右偏移213像素(第12行);

通常,左、右、上、下边距的确切尺寸将直接在XML中设定。

按照相同的方法,创建以下视图 [1]:

 

组件如下:

Id
Type
Rôle
1
textViewTitreVue1
TextView
Titre de la vue
2
textView1
TextView
une question
3
editTextNom
EditText
saisie d'un nom
4
buttonValider
Button
pour valider la saisie
5
buttonVue2
Button
pour passer à la vue n° 2

调整组件之间的相对位置可能是一项令人沮丧的任务,因为图形编辑器的反应有时会出人意料。使用组件的属性可能更为理想:

组件 [textView1] 应放置在标题下方 50 像素处,并距离容器左边缘 50 像素:

  • 在 [1] 中,该组件的顶部 (top) 与 [textViewTitreVue1] 组件的底部 (bottom) 对齐,距离为 50 像素 [3] (top);
  • 在 [2] 中,该组件的左边缘(left)与容器的左边缘对齐,间距为 50 像素 [3](left);

组件 [editTextNom] 应位于组件 [textView1] 右侧 60 像素处,并与其底部对齐;

 
  • 在 [1] 中,该组件的左边缘(left)与 [textView1] 组件的右边缘(right)对齐,间距为 60 像素 [2](left)。 它与组件 [textView1] 的底部(bottom:bottom)对齐;

组件 [buttonValider] 应位于组件 [editTextNom] 右侧 60 像素处,并沿底部与该组件对齐;

 
  • 在 [1] 中,该组件的左边缘(left)与 [editTextNom] 组件的右边缘(right)对齐,间距为 60 像素 [2](left)。 它与组件 [editTextNom] 的底部对齐(bottom:bottom);

组件 [buttonVue2] 应位于组件 [textView1] 下方 50 像素处,并与其左对齐;

 
  • 在 [1] 中,该组件的左边缘(left)与 [textView1] 组件的左边缘(left)对齐,并位于其下方(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 行:该业务活动现在显示的是视图 [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] 时立即弹出键盘。因为该视图包含一个输入字段,在视图显示时该字段处于焦点状态。默认情况下,焦点会触发虚拟键盘的弹出;

运行应用程序并确认显示的确实是视图 [vue1.xml]:

Image

1.5.3. 事件处理

现在,我们来处理视图 [Vue1] 中按钮 [Valider] 的点击事件:

Image

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

  // 视觉界面的元素
  @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");
  }

  // 事件管理器
  @Click(R.id.buttonValider)
  protected void doValider() {
    // 显示输入的名称
    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)] 指定了处理标识符 [R.id.buttonValider] 按钮上“Click”事件的方法。该标识符同样来自视图 [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 的第一个参数是活动,
    • 第二个参数是要在makeText显示的对话框中显示的文本
    • 第三个参数是显示框的存活时间:Toast.LENGTH_LONG Toast.LENGTH_SHORT
  • 第26行,注解 [@AfterViews] 标注了当所有由 [@ViewById] 标注的字段初始化完成时应执行的方法。了解这些字段何时初始化非常重要。 例如,在方法 [onCreate] 中能否使用第 18 行的引用?为解答此问题,我们添加了日志;

请运行项目 [Exemple-04],并确认点击按钮 [Valider] 时是否发生某些操作。我们获得了以下日志:

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

由此可得出结论:当方法 [onCreate] 执行时,由 [@ViewById] 注释的字段尚未初始化。再次提醒初学者,建议在管理应用程序生命周期的方法中添加此类日志。

1.6. 示例-05:视图间的导航

在上一项目中,[Vue n° 2]按钮尚未被利用。我们计划通过创建第二个视图并演示如何在视图之间导航来加以利用。解决此问题有多种方法。这里建议的方法是将每个视图与一个活动关联起来。 另一种方法是创建一个名为 [AppCompatActivity] 的单一活动,用于显示 [Fragment] 类型的视图。这将是未来应用程序中采用的方法。

1.6.1. 创建项目

将项目 [Exemple-04] 复制为 [Exemple-05]。 为此,我们将遵循第 1.4 节中描述的将 [Exemple-02] 复制为 [Exemple-03] 的步骤,该步骤已在第 1.5 节中重述。

1.6.2. 添加第二个活动

为了管理第二个视图,我们将创建第二个活动。该活动将负责管理第 2 号视图。这里采用的是“一个视图 = 一个活动”的模式。当然,还有其他可能的模式。

123

Image

  • 在 [1-4] 中,创建一个新活动;

Image

  • 在 [5] 中,指定将生成的类名;
  • 在 [6] 中,指定与新业务活动关联的视图名称(vue2.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 行:该活动关联到视图 [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 行,已注册第二个 Activity。

1.6.3. 从视图 1 导航至视图 2

让我们回到显示视图 1 的 [MainActivity] 类的代码。目前尚未处理跳转到视图 2 的逻辑:

  

我们通过以下方式实现该功能:


  // 导航至视图 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
    // 导航至视图 2,并将视图 1 中输入的名称传递给它
    // 创建一个 Intent
    Intent intent = new Intent();
    // 将该 Intent 关联到一个 Activity
    intent.setClass(this, SecondActivity.class);
    // 为该 Intent 关联信息
    intent.putExtra("NOM", editTextNom.getText().toString().trim());
    // 通过传递 Intent 来启动类型为 [SecondActivity] 的 Activity
    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"/>

注释描述了视图切换所需执行的步骤:

  1. 第 6 行:创建一个类型为 [Intent] 的对象。该对象将用于指定要启动的活动以及传递给该活动的参数;
  2. 第 8 行:将 Intent 关联到一个 Activity,此处为类型为 [SecondActivity] 的 Activity,其负责显示视图 2。需注意,Activity [MainActivity] 负责显示视图 1。因此,一个视图对应一个 Activity。 我们需要定义类型 [SecondActivity];
  3. 第10行:可选操作,在对象[Intent]中填入信息。这些信息将用于即将启动的活动[SecondActivity]。[Intent.putExtra]的参数为(键对象,值对象)。 需注意,方法 [EditText.getText()] 返回输入框中输入的文本时,返回的类型并非 [String],而是 [Editable]。 需使用方法 [toString] 才能获取输入的文本;
  4. 第 12 行:启动由对象 [Intent] 定义的活动。

运行项目 [Exemple-05],并确认是否成功显示视图 2(目前为空):

1.6.4. 视图 2 的构建

 
  • 在 [1-2] 中,我们删除不再需要的视图 [main.xml],然后按以下方式修改视图 [vue2.xml]:
 

组件如下:

Id
Type
Rôle
1
textViewTitreVue2
TextView
Titre de la vue
2
textViewBonjour
TextView
un texte
5
btn_vue1
Button
pour passer à la vue n° 1

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

运行项目 [Exemple-05],并点击按钮 [Vue n° 2],确认是否成功显示新视图。

1.6.5. [SecondActivity] 活动

在 [MainActivity] 中,我们编写了以下代码:


    // 跳转至视图 2
    protected void navigateToView2() {
        // 跳转至视图 2,并向其传递在视图 1 中输入的名称
        // 创建一个 Intent
        Intent intent = new Intent();
        // 将该 Intent 关联到一个 Activity
        intent.setClass(this, SecondActivity.class);
        // 为该 Intent 关联信息
        intent.putExtra("NOM", edtNom.getText().toString().trim());
        // 通过传递 Intent 来启动类型为 [SecondActivity] 的 Activity
        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 {

  // 视觉界面的组件
  @ViewById
  protected TextView textViewBonjour;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }

  @AfterViews
  protected void afterViews() {
    // 若存在,则获取该 Intent
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // 获取名称
        String nom = extras.getString("NOM");
        if (nom != null) {
          // 显示名称
          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 行)中,无法使用这些字段,因为它们尚未被初始化。 在项目 [Exemple-05] 中,系统会在不同活动之间切换,因此最初并不清楚注解为 [@AfterViews] 的方法是在活动初始实例化时执行一次,还是在每次启动活动时执行。 测试结果证实了第二种假设;
  • 第 26 行:类 [AppCompatActivity] 包含方法 [getIntent],该方法返回与该活动关联的对象 [Intent];
  • 第28行:方法[Intent.getExtras]返回类型[Bundle],该类型是一种字典,包含与业务活动对象[Intent]相关联的信息;
  • 第 31 行:获取活动对象 [Intent] 中存储的名称;
  • 第 34 行:将其显示出来。

提醒:标注为 [@ViewById] 的字段中不得包含带重音的字符

让我们回到类 [SecondActivity]。因为我们写了:


@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {

AA 将生成一个从 [SecondActivity] 派生的类 [SecondActivity_],而该类才是真正的业务活动。这导致我们需要在以下位置进行修改:

[MainActivity]


  // 跳转至视图 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
..
    // 将该 Intent 关联到一个 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() {
    // 为 [MainActivity] 活动创建 Intent
    Intent intent1 = new Intent();
    intent1.setClass(this, MainActivity_.class);
    // 获取当前活动的 Intent [SecondActivity]
    Intent intent2 = getIntent();
    if (intent2 != null) {
      Bundle extras2 = intent2.getExtras();
      if (extras2 != null) {
        // 将名称放入 [MainActivity] 的 Intent 中
        intent1.putExtra("NOM", extras2.getString("NOM"));
      }
      // 调用 [MainActivity]
      startActivity(intent1);
    }
}
  • 第 1-2 行:将方法 [navigateToView1] 与按钮 [btn_vue1] 的点击事件关联;
  • 第 4 行:创建新的 [Intent];
  • 第 5 行:关联到 [MainActivity_] 活动;
  • 第 7 行:获取与 [SecondActivity] 关联的 Intent;
  • 第 9 行:获取该 Intent 的信息;
  • 第12行:从[intent2]中获取键[NOM],并将其与相同关联值一起放入[intent1]中;
  • 第15行:启动[MainActivity_]活动。

在 [MainActivity] 的代码中,添加了以下 [@AfterViews] 方法:


  @AfterViews
  protected void afterViews() {
    // 若存在,则获取该Intent
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // 获取名称
        String nom = extras.getString("NOM");
        if (nom != null) {
          // 显示
          editTextNom.setText(nom);
        }
      }
    }
}

完成这些修改并测试您的应用程序。现在,当从视图 2 返回视图 1 时,应能恢复最初输入的名称,而此前并非如此。

1.6.7. 活动的生命周期

我们在第 1.3.5 节中介绍了活动的生命周期。这里有两个活动,在执行过程中会在它们之间切换。 这两个活动包含两个方法,我们不太清楚它们何时相互调用:[onCreate] 和 [afterViews]。了解这一点非常重要。为此,我们在两个活动中添加日志:

因此,在类 [MainActivity] 中,我们编写:


  // 制造商
  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] 中也进行完全相同的操作。

应用程序启动时,我们得到以下日志:

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

第一个活动的 [onCreate, afterViews] 方法按此顺序执行。点击 [Vue n° 2] 按钮后,新日志如下:

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

第二个活动的 [onCreate, afterViews] 方法已按此顺序执行。点击 [Vue n° 1] 按钮后,新日志如下:

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

因此,[MainActivity]类再次被实例化。点击[Vue n° 2]按钮后,新的日志如下:

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

因此,类 [SecondActivity] 再次被实例化。

因此,在切换活动时,这两个活动都会被系统性地重新创建。

接下来我们将探讨一种架构,其中仅有一个活动即可管理多个称为片段的视图。与之前的方法(一个活动可能被实例化多次)不同,该活动和视图仅会被实例化一次。

1.7. 示例-06:标签页导航

接下来我们将探讨标签页界面。虽然该示例较为复杂,但涵盖了后续将用到的所有要素:单一活动、片段(视图)管理器、片段容器以及片段间的导航。标签页的概念与片段不同,且在本示例中仅作为次要内容进行展示。

1.7.1. 创建项目

我们创建一个新项目:

 
  • 在 [7] 中,选择带标签页的活动(Tabbed Activity);
  • 在 [10-14] 中,保留默认值;
  • 在 [15] 中,选择带有标题栏的标签页;

生成的项目如下:

 
  • 在 [1] 中,活动;
  • 在 [2] 中,视图;

系统已自动创建了一个名为 [app] 的执行配置(与模块同名):

 

可以执行它。随后将显示一个带有三个选项卡的窗口 [3-6]:

Image

1.7.2. Gradle 配置

项目 [Exemple-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] 关联的视图。在 [design] 模式下,该视图如下所示:

Image

它包含以下组件:

  
  • [main_content] 即视图的全部内容;
  • [appbar](红色框 1)是应用程序栏。它包含两个组件:
    • [toolbar](黄色框 4)是工具栏;
    • [tabs](橙色框 5)是标签页标题栏;
  • [container](绿色框,2)可容纳各种片段。片段即视图。因此,同一活动可在该容器中显示多个视图(片段);
  • [fab](组件 3)被称为浮动组件;

在 [text] 模式下,代码如下:


<?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 行)的 layout(组件布局管理器);
  • 第 11-33 行:[appbar] 容器(第 12 行)。它是一个类型为 [AppBarLayout](第 11 行)的 layout
  • 第18-24行:[toolbar]组件(第19行),其类型为[Toolbar](第18行);
  • 第28-31行:容器[tabs](第29行)。这是一个类型为[TabLayout](第28行)的layout。它将显示选项卡的标题;
  • 第35-39行:组件[container](第36行)。该容器用于显示活动的不同视图;
  • 第 41-47 行:类型为 [FloatingActionButton](第 41 行)的组件 [fab](第 42 行)。这是一个可点击的按钮。默认情况下,它位于整体视图的右下角;

我们不会试图理解这些组件所有属性的含义。我们将直接使用它们。通常需要通过实践,且往往在 [design] 模式下,才能发现它们的作用。在此模式下,我们会发现这些组件拥有数十个属性。通常只有部分属性被初始化,其余则保持默认值。

不过,我们还是需要说明几点。配置不同视图的大部分值都集中在 [res / values] 文件夹中:

  

这些值在文件 [activity_main.xml] 的第 15-16 行、第 23 行、第 39 行和第 46 行中被引用。举个例子:

  • 第 15 行:

    android:paddingTop="@dimen/appbar_padding_top"

注释 [@dimen] 引用了文件 [res / values / dimens.xml]:


<resources>
  <!-- 默认屏幕边距,遵循 Android 设计规范。 -->
  <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. 活动

  

为该活动生成的代码与前文描述的视图一样:相当复杂。我们将分几个步骤进行分析。

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 {

  // 片段管理器
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // 片段容器 
  private ViewPager mViewPager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      // 父级
    super.onCreate(savedInstanceState);
    // 视图
    setContentView(R.layout.activity_main);
    // 工具栏
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // 片段管理器
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // 片段容器与片段管理器相关联
    // 也就是说,片段容器的第 i 个片段就是片段管理器提供的第 i 个片段
    mViewPager = (ViewPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // 标签栏也与片段容器相关联
    // 即第 i 个标签页显示容器中的第 i 个片段
    TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
    tabLayout.setupWithViewPager(mViewPager);
   }


  // 一个片段
  public static class PlaceholderFragment extends Fragment {
 ...
  }

  // 片段管理器
  // 主视图中要显示的片段正是由它提供的
  //必须定义方法 [getItem] 和 [getCount]——其余方法为可选
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
  }
}
  • 第 28 行:Android 提供了一个 [android.support.v4.view.ViewPager] 类型的视图容器(第 12 行)。必须为该容器提供一个视图或片段管理器。该管理器由开发人员提供;
  • 第 25 行:本示例中使用的片段管理器。其实现位于第 61-63 行;
  • 第 31 行:在创建 Activity 时执行的方法;
  • 第 35 行:将视图 [activity_main.xml] 关联到该活动;
  • 第 37 行:通过其标识符获取视图中 [toolbar] 组件的引用;
  • 第 38 行:该工具栏成为该活动的操作栏(Android 概念);
  • 第 40 行:实例化片段管理器。构造函数的参数是 Android 类 [android.support.v4.app.FragmentManager](第 10 行);
  • 第 44 行:在视图 [activity_main.xml] 中通过其标识符获取片段容器的引用;
  • 第 45 行:将片段管理器与片段容器关联。这意味着当要求片段容器显示第 i 个片段时,该请求将转交给片段管理器;
  • 第 48 行:通过标识符获取标签栏的引用;
  • 第 49 行:标签管理器与片段容器相关联。这意味着当点击第 i 个标签时,容器将显示第 i 个片段。标签管理器与片段容器之间的关联使我们无需进行任何标签管理。因此,我们无需为点击标签定义事件处理程序。 与片段容器的关联默认提供了此功能。我们将看到一个片段数量多于标签数量的示例。在这种情况下,我们不进行此关联。

片段管理器 [SectionsPagerAdapter] 如下所示:


// 片段管理器
  // 系统会向其请求在主视图中显示的片段
  // 必须定义方法 [getItem] 和 [getCount] - 其余为可选
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }

    // 片段编号 位置
    @Override
    public Fragment getItem(int position) {
      // 实例化一个片段 [PlaceHolder] 并返回它
      return PlaceholderFragment.newInstance(position + 1);
    }

    // 返回管理的片段数量
    @Override
    public int getCount() {
      return 3;
    }

    // 可选 - 为管理的片段指定标题
    @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] 的 layout
  • 第 11-14 行:视图的唯一组件(片段):一个由 [section_label] 标识的 [TextView];

在 [MainActivity] 中,管理的片段属于以下 [PlaceholderFragment] 类型:


// 一个片段
  public static class PlaceholderFragment extends Fragment {
      // 片段中显示的文本
    private static final String ARG_SECTION_NUMBER = "section_number";

    public PlaceholderFragment() {
    }

    // 返回一个包含信息的片段:作为参数传递的片段编号
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // 片段
      PlaceholderFragment fragment = new PlaceholderFragment();
      // 嵌入信息
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // 结果
      return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // 视图 [fragment_main] 已实例化
      View rootView = inflater.inflate(R.layout.fragment_main, container, false);
      // 已找到 [TextView]
      TextView textView = (TextView) rootView.findViewById(R.id.section_label);
      // 其内容被修改
      textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
      // 返回视图
      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] 组件,通过其标识符获取该组件的引用;
  • 第 29 行:在 [TextView] 中显示文本;
    • [getString] 是父类 [AppCompatActivity] 的一个方法;
    • 第一个参数是组件编号。[R.string.section_format] 指代在文件 [res / values / strings.xml](下文第 4 行)中由 [section_format] 标识的组件编号:

<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 行):

    // 返回包含信息的片段:作为参数传递的片段编号
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // 片段
      PlaceholderFragment fragment = new PlaceholderFragment();
      // 嵌入信息
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // 结果
      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) {
    // 加载菜单;如果存在操作栏,则向其中添加项目。
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // 在此处处理操作栏项目的点击。操作栏将
    // 自动处理“主页/向上”按钮的点击,只要
    // 在 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");
    // 加载菜单;如果存在操作栏,这会向其中添加项目。
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // 在此处处理操作栏项目的点击。操作栏将
    // 只要您在 AndroidManifest.xml 中指定了父活动,
    // 在 AndroidManifest.xml 中指定了父活动。
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      Log.d("menu", "action_settings selected");
      return true;
    }
    // 父级
    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] 中按以下方式进行管理:


    // 浮动按钮
    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. 项目运行

现在我们已经解释了生成的代码的细节,可以更好地理解其执行过程:

Image

当点击第 i 个标签页时,第 i 个片段会在视图容器中显示。这从 [4] 中显示的文本可见一斑。此外,还可以注意到,通过用鼠标向右或向左拖动视图(滑动),可以在各个标签页之间切换。 我们将看到可以控制这种行为。

点击[6]中的菜单选项时,会生成以下日志:

 

1.7.6. 片段的生命周期

  • 在 [1] 中,可以看到方法 [onCreateView] 及其后续方法会在片段首次显示时以及每次活动需要重新显示该片段时执行;

为了追踪活动和片段的生命周期,我们在 [MainActivity] 的代码中添加了以下日志:


// 构造函数
  public MainActivity(){
    Log.d("MainActivity","constructor");
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity","onCreate");
      // 父级
    super.onCreate(savedInstanceState);
...
  }

  // 一个片段
  public static class PlaceholderFragment extends Fragment {
    // 片段中显示的文本
    private static final String ARG_SECTION_NUMBER = "section_number";

    public PlaceholderFragment() {
      Log.d("PlaceholderFragment", "constructor");
    }

    // 返回包含信息的片段:作为参数传递的片段编号
    public static PlaceholderFragment newInstance(int sectionNumber) {
      Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
      // 片段
      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
2
3
4
5
6
7
8
9
05-28 10:44:32.622 29371-29371/exemples.android D/MainActivity: constructor
05-28 10:44:32.626 29371-29371/exemples.android D/MainActivity: onCreate
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: newInstance 1
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: newInstance 2
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 10:44:32.760 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 10:44:33.349 29371-29371/exemples.android D/menu: création menu en cours
  • 第 1 行:创建业务活动;
  • 第 2 行:执行其方法 [onCreate];
  • 第3-4行:实例化片段1;
  • 第 5-6 行:实例化片段 2;
  • 第 7 行:初始化片段 2;
  • 第 8 行:初始化片段 1;
  • 第 9 行:创建活动菜单;

这里需要回顾一下用于创建片段的代码:


  // 片段管理器
  // 主视图中要显示的片段由它负责提供
  // 必须定义方法 [getItem] 和 [getCount] ——其余方法为可选
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }

    // 片段编号 位置
    @Override
    public Fragment getItem(int position) {
      // 实例化一个片段 [PlaceHolder] 并将其渲染
      return PlaceholderFragment.newInstance(position + 1);
    }
...
  • 第11-15行:每当片段容器请求时,[newInstance]都会实例化一个片段;

上述日志显示,前两个片段已被实例化并初始化。

现在,点击第 2 个标签页。新的日志如下:

1
2
3
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: newInstance 3
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 3
  • 第 1-3 行:片段 3 被实例化并初始化。需要提醒的是,此时显示的是片段 2;

现在,点击第 3 个标签页。这里没有任何日志。这可能是因为待显示的第 3 个片段已经实例化过了。现在,回到第 1 个标签页。此时日志如下:

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

片段 1 并未被重新实例化,但其方法 [onCreateView] 再次被执行。这种行为在另外两个片段中也会重现。

从这些日志中,我们可以得出以下结论:

  • 该活动被实例化并初始化了一次;
  • 每个片段均被实例化一次;
  • 每个片段的 [onCreateView] 方法均被执行了多次;

需要了解且日志也证实的是:默认情况下,当显示第 i 个片段时,如果片段 i-1 和 i+1 尚未实例化,则会对其进行实例化。 这解释了例如在启动时,虽然需要显示第 1 号片段,但实际被实例化和初始化的却是第 1 号和第 2 号片段。日志还显示,即使第 i 号片段被显示多次,方法 [getItem(i)] 也仅被调用一次。 因此,负责显示第 i 个片段的片段容器 [ViewPager] 似乎仅向片段管理器 [SectionsPagerAdapter] 请求该片段一次。此后它不再重新请求,而是继续使用已获取的片段。

最后,日志提供了关于片段方法 [onCreateView] 的信息:

  • 启动时,片段 1 和 2 已被实例化,且其 [onCreateView] 方法已执行;
  • 当从片段1切换到片段2时,片段2的[onCreateView]方法不会被重新执行。 因此无法利用该方法更新片段 2。然而,用户可能已在片段 1 中执行了某项操作,其结果本应由片段 2 显示。可见,方法 [onCreateView] 无法用于更新片段 2。必须寻找其他解决方案;

1.8. 示例-07:使用库 [AA] 重写示例-06

1.8.1. 创建项目

我们将项目 [Exemple-06] 复制为 [Exemple-07],以便在后者中引入 Android 注释。为此,请按照第 1.4 节中的步骤操作。我们得到以下结果:

1.8.2. Gradle 配置

 

我们将文件 [build.gradle] 修改如下:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // 自 Android 的 Gradle 插件 0.11 起,必须使用 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 {

  // 片段管理器
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // 片段容器
  @ViewById(R.id.container)
  protected MyPager mViewPager;
  // 标签页管理器
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // 悬浮按钮
  @ViewById(R.id.fab)
  protected FloatingActionButton fab;


  // 构造函数
  public MainActivity() {
    Log.d("MainActivity", "constructor");
  }

  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");

    // 工具栏
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // 片段管理器
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // 片段容器与片段管理器相关联
    // 即片段容器的第 i 个片段是片段管理器提供的第 i 个片段
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // 标签栏也与片段容器相关联
    // 即第 i 个标签页显示容器中的第 i 个片段
    tabLayout.setupWithViewPager(mViewPager);

    // 浮动按钮
    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_main.xml] 的标识符;
  • 第 11-12 行:标识为 [R.id.tabs] 的组件被注入到字段 [tabLayout] 中。这是标签页管理器;
  • 第 14-15 行:标识为 [R.id.fab] 的组件被注入到字段 [fab] 中。这是浮动按钮;
  • 第23-50行:原先位于方法[onCreate]中的代码迁移到了一个名称任意但带有注解[@AfterViews]的方法中(第23行)。 在该注释方法中,可确保所有被 [@ViewById] 注释的视觉界面组件均已初始化;
  • 此外,我们还添加了日志以观察活动的生命周期;

需要提醒的是,注解 [@EActivity] 将生成一个名为 [MainActivity_] 的类,该类将成为项目的实际活动。因此,需按以下方式修改文件 [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 行:新的活动。

此时,请重新运行项目,并确认是否仍能获得带标签页的界面。

1.8.4. 重写片段

我们将重新审视项目中片段的管理。目前,类 [PlaceholderFragment] 是活动 [MainActivity] 的静态内部类。 我们将回归到更常见的使用场景,即片段在外部类中定义的情况。此外,我们将为片段引入 AA 注解。

项目 [Exemple-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;

// 片段是由片段容器显示的视图
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

  // 视觉界面的组件
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;

  // 片段编号
  private static final String ARG_SECTION_NUMBER = "section_number";

  // 构造函数
  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)));
    // 父级
    super.onResume();
    // 视图
    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行: 将由 [R.id.section_label] 标识的 [fragment_main.xml] 组件的引用注入到字段 [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 {

    // 片段
    private Fragment[] fragments;
    // 片段数量
    private static final int FRAGMENTS_COUNT = 3;
    // 片段编号
    private static final String ARG_SECTION_NUMBER = "section_number";

    // 制造商
    public SectionsPagerAdapter(FragmentManager fm) {
      // 父级
      super(fm);
      // 片段数组的初始化
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length; i++) {
        // 创建片段
        fragments[i] = new PlaceholderFragment_();
        // 可以向片段传递参数
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
    }

    // 片段编号位置
    @Override
    public Fragment getItem(int position) {
        Log.d("MainActivity", String.format("getItem[%s]", position));      
      return fragments[position];
    }

    // 返回管理的片段数量
    @Override
    public int getCount() {
      return fragments.length;
    }

    // 可选 - 为管理的片段指定标题
    @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 文件夹中(需切换到 [Project] 视图才能看到 [2]);

运行项目 [Exemple-07] 并确认其仍能正常运行。

1.8.5. 日志检查

启动应用程序时,日志如下:

05-28 13:54:54.801 8809-8809/exemples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/exemples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/exemples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/exemples.android D/menu: création menu en cours
  • 第 1 行:构建唯一活动;
  • 第 2 行:活动中的 [afterViews] 方法:由 [@ViewById] 注释的字段被初始化;
  • 第 3-5 行:构建三个片段;
  • 第6-7行:片段容器[ViewPager]请求前两个片段;
  • 第8-9行:片段2的方法;
  • 第10-11行:片段1的方法;
  • 第 12-13 行:片段 1 的方法 [onResume];
  • 第 14-15 行:片段 2 的 [onResume] 方法;
  • 第16行:创建活动菜单;

值得注意的是,这回答了之前提出的一个问题:例如片段1中的方法[onResume](第12行)是在片段中的方法[afterViews](第11行)之后执行的。 因此,当方法 [onResume] 执行时,它可以使用由 [@ViewById] 标注的字段。因此,我们现在可以按以下方式编写方法 [onResume]:


  @Override
  public void onResume() {
    Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // 父级
    super.onResume();
    // 显示
    textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}

现在让我们从选项卡 1 切换到选项卡 2。新的日志如下:

1
2
3
4
5
05-28 14:01:42.786 8809-8809/exemples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3
  • 第 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个标签页。此时日志如下:

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

可以看到片段 1 的所有方法均已执行。可见方法 getItem 未被调用。如前所述,该方法对每个片段仅调用一次;

现在,从标签页 1 切换到相邻的标签页 2。日志如下:

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

令人惊讶吧?片段 3 的所有方法都被重新执行了。

要理解这些现象,需记住:默认情况下,当片段容器准备显示片段 i 时,会初始化片段 i-1、i 和 i+1。让我们结合这一信息重新审视日志。

首先是应用程序启动时的日志:

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

由于片段容器将显示片段 1,因此片段 1 和 2 被初始化(第 8-15 行)。

现在从标签页 1 切换到标签页 2:

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

由于片段容器将显示片段 2,因此片段 1、2 和 3 必须被初始化。片段 1 和 2 在上一步骤中已初始化。片段 3 在第 2-5 行被初始化。

从标签页 2 切换到标签页 3。没有日志。由于片段容器将显示片段 3,因此片段 2 和 3 必须被初始化。 但自上一步骤起,它们已处于初始化状态。此处未显示的是:与片段3不相邻的片段1会丢失其状态,该状态未保存在内存中。

从标签页 3 切换到标签页 1。日志如下:

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

由于片段容器将显示片段 1,片段 2 也必须被初始化。它已在上一步中完成初始化。 在此步骤中,片段1的状态已丢失,因此会在第1-4行被重置。这里未显示的是:与片段1不相邻的片段3会丢失其状态,该状态因此不会保存在内存中。

当从标签页1切换到相邻的标签页2时,日志如下:

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

由于片段容器将显示片段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;

// 片段是由片段容器显示的视图
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

...

  @Override
  public void onDestroyView() {
    // 日志
    Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // 父级
    super.onDestroyView();
  }

}

运行应用程序。初始日志如下:

06-03 02:45:42.163 2346-2346/exemples.android D/MainActivity: constructor
06-03 02:45:42.331 2346-2346/exemples.android D/MainActivity: afterViews
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.515 2346-2346/exemples.android D/MainActivity: getItem[0]
06-03 02:45:42.516 2346-2346/exemples.android D/MainActivity: getItem[1]
06-03 02:45:42.517 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 02:45:42.520 2346-2346/exemples.android D/PlaceholderFragment: afterViews 2
06-03 02:45:42.523 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:45:42.525 2346-2346/exemples.android D/PlaceholderFragment: onResume 2
06-03 02:45:42.525 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 02:45:44.596 2346-2346/exemples.android D/menu: création menu en cours
  • 第 1 行:构建唯一的活动;
  • 第 2 行:活动中的 [afterViews] 方法:其由 [@ViewById] 注解的字段被初始化;
  • 第 3-5 行:构建三个片段;
  • 第6-7行:片段容器[ViewPager]请求前两个片段;
  • 第 8-9 行:创建片段 2 的视图(不一定显示出来);
  • 第 10-11 行:创建片段 1 的视图(不一定显示出来);
  • 第12-13行:片段1的[onResume]方法;
  • 第14-15行:片段2的[onResume]方法;
  • 第 16 行:创建活动菜单;

从标签页 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 行:片段容器请求第 3 个片段;
  • 第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行:片段1的生命周期被重新执行,因为它之前经历了[onDestroyView];
  • 第5行:现在片段3的[onDestroyView]方法被执行。同样地,当用户返回片段3或相邻片段时,该片段的生命周期将被重新执行;

1.8.7. setUserVisibleHint

生命周期中的 [onCreateView] 方法会实例化与片段关联的视图,但并不一定使其可见。接下来我们将探讨这一点。每当片段的可见性发生变化时,都会执行 [Fragment.setUserVisibleHint] 方法。我们将此方法添加到片段代码中:


package exemples.android;

....

// 片段是由片段容器显示的视图
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

  // 视觉界面的组件
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;

  ...

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // 日志
    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 行的日志显示,只有片段 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].setOffscreenPageLimit(n);

使用上述指令,

  1. 当片段容器 [ViewPager] 准备显示第 i 个片段时,如果尚未执行,则执行区间 [i-n, i+n] 内相邻片段的生命周期;
  2. 随后若显示片段 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) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // 片段管理器
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // 片段容器与片段管理器相关联
    // 即片段容器的第 i 个片段是片段管理器提供的第 i 个片段
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // 禁止片段间的滑动操作
    mViewPager.setSwipeEnabled(false);

    // 片段偏移量
    mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);

    // 标签栏也与片段容器相关联
    // 即第 i 个标签页显示容器中的第 i 个片段
    tabLayout.setupWithViewPager(mViewPager);

    // 浮动按钮
    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()。因此,所有片段都将被初始化。当显示窗口移动到另一个片段时,片段容器:
    • 将发现新片段的所有相邻片段均已初始化,因此不会对其进行初始化;
    • 由于新片段的邻接范围同样覆盖了所有片段,因此片段容器不会对任何片段进行“反初始化”;

总的来说,我们应该看到所有片段都在应用程序启动时被实例化和初始化,之后就再也不会发生了。现在我们通过查看日志来验证这一点。

启动时,我们得到以下日志:

06-03 03:30:55.411 10344-10344/exemples.android W/System: ClassLoader referenced unknown path: /data/app/exemples.android-1/lib/x86
06-03 03:30:55.417 10344-10344/exemples.android D/MainActivity: constructor
06-03 03:30:55.460 10344-10344/exemples.android D/MainActivity: afterViews
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.559 10344-10344/exemples.android D/MainActivity: getItem[0]
06-03 03:30:55.559 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/MainActivity: getItem[1]
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/MainActivity: getItem[2]
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 03:30:56.798 10344-10344/exemples.android D/menu: création menu en cours
  • 第 4-6 行:构建三个片段;
  • 第 7、9、11 行:片段容器请求这三个片段。在之前的版本中,它只请求了两个;
  • 第14-25行:三个片段的生命周期正在执行;

现在让我们从标签页 1 切换到标签页 2:

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

从标签页 2 切换到标签页 3:

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

然后从标签页 3 切换到标签页 1:

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

日志证实了这一推测。所有片段都在启动时被实例化并初始化。此后,其生命周期方法不再被执行。这种片段的运行方式非常可预测,极大地简化了它们的使用。

我们需要找到一种方法,无论开发者选择了何种片段邻接关系,都能更新即将显示的片段。日志向我们揭示了两点:

  • 方法 [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
  • 可见,当片段 1 变为可见时,其视图尚未创建。因此无法对其进行操作。这可以在片段的生命周期中完成,例如在方法 [onCreateView](第 11 行)或 [onResume](第 13-14 行)中。 由于我们使用了注解 AA,通常无需编写方法 [onCreateView]。因此,方法 [onResume] 似乎是此处更新片段 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

在此,需使用片段1(第6-7行)中的方法[onResume]来更新片段1。

因此,在这个示例中,我们可以看到,要更新即将显示的片段,有两种方法可用:[setUserVisibleHint] 和 [onResume]。

我们将在此新项目中实现该方案,每个片段需显示其被调用的次数(即“访问”次数)。因此,每次片段被调用时,都需更新其显示内容。这正是我们试图解决的问题。

在此之前,我们先探讨一下活动或片段生命周期的最后阶段——即被销毁的阶段。如果其他优先级更高的活动请求了不可用的资源,系统可能会主动销毁某个活动。为了释放这些资源,系统将主动销毁某些活动。 此时,将调用该活动和片段的 [onDestroy] 方法。

1.8.9. OnDestroy

我们将允许用户通过菜单选项 [5] 删除该活动。为此,我们在文件 [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");
    // 在此处处理操作栏项的点击。操作栏将
    // 自动处理“主页/向上”按钮的点击,只要
    // 在 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");
      //结束该活动
      finish();
      return true;
    }
    // 父级
    return super.onOptionsItemSelected(item);
}
  • 第14-19行:复制粘贴第10-13行的代码,并根据新选项调整代码;
  • 第 17 行:通过软件操作结束该活动;

现在运行此新版本,待第一个视图显示后,点击菜单选项 [Terminate]。此时日志如下:

1
2
3
4
5
6
7
8
9
06-04 12:35:32.996 15994-15994/exemples.android D/menu: onOptionsItemSelected
06-04 12:35:32.996 15994-15994/exemples.android D/menu: action_terminate selected
06-04 12:35:33.561 15994-15994/exemples.android D/MainActivity: onDestroy
06-04 12:35:33.561 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 1
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 1
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 2
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 2
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 3
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 3
  • 第1-2行:点击选项[Terminate];
  • 第 4 行:调用该活动的 [onDestroy] 方法;
  • 第4-5行:调用片段1的[onDestroyView]方法,随后调用其[onDestroy]方法;
  • 第6-9行:此操作将对另外两个片段重复执行;

因此需注意,当活动被系统、开发者或用户删除时,会调用活动及片段的 [onDestroy] 方法。可利用此方法保存信息(例如保存在平板电脑本地),以便用户重新启动应用时能够找回这些信息。

1.9. 示例-08:更新具有可变片段邻接关系的片段

1.9.1. 创建项目

将项目 [Exemple-07] 复制为 [Exemple-08]。为此,请遵循第 1.4 节中描述的将 [Exemple-02] 复制为 [Exemple-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;

// 片段是由片段容器显示的视图
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

  // 视觉界面的组件
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  // 数据
  private boolean afterViewsDone = false;
  private boolean initDone = false;
  private String text;
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private int numVisit = 0;

  // 片段编号
  private static final String ARG_SECTION_NUMBER = "section_number";

  // 构造函数
  public PlaceholderFragment() {
    Log.d("PlaceholderFragment", "constructor");
  }


  @AfterViews
  protected void afterViews() {
    // 内存
    afterViewsDone = true;
    // 日志
    Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    if (!initDone) {
      // 初始文本
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // 初始化完成
      initDone = true;
    }
    // 当前文本显示
    textViewInfo.setText(text);
  }


  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }

  @Override
  public void onDestroyView() {
...
  }

  @Override
  public void onResume() {
...
  }

  // 更新片段
  public void update() {
    // 待办工作取决于访问编号
    if (numVisit > 1) {
      // 日志
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // 文本已修改
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }

  // 日志的本地信息
  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] 如下:


  // 数据
  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) {
    // 父级
    super.setUserVisibleHint(isVisibleToUser);
    // 内存
    this.isVisibleToUser = isVisibleToUser;
    // 日志
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // 访问次数
    if (isVisibleToUser) {
      // 增量
      numVisit++;
      // 更新片段
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // 片段将被隐藏
      updateDone = false;
    }
  }

  @Override
  public void onResume() {
    // 父级
    super.onResume();
    // 日志
    Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // 更新
    if (isVisibleToUser && !updateDone) {
      update();
      updateDone = true;
    }
}
  • 第 14 行:记录片段的可见状态;
  • 第 22-25 行: 如果片段可见且已执行方法 [@AfterViews],则执行方法 [update],并将布尔值 [updateDone] 传递给 true
  • 第 26-28 行:如果片段将被隐藏,则将布尔值 [updateDone] 重置为 false。 实际上,我们需要一个事件,以便在调用方法 [update] 时,将布尔值 [updateDone](此前已设置为 true)重置为 false,从而允许进行新的更新。 我们利用片段不再可见这一事实来实现这一点。当片段再次可见时,必须重新执行片段更新;
  • 第 32-42 行:日志显示,根据片段所选的邻接关系,方法 [onResume] 可能在片段不可见时执行。 如果片段不可见,则不进行更新(第39行),并且与处理[setMenuVisibility]的方法一样,管理布尔值[updateDone]。

最后,方法 [onDestroyView] 如下:


  @Override
  public void onDestroyView() {
    // 父级
    super.onDestroyView();
    // 更新标记
    afterViewsDone = false;
    // 日志
    Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}

当片段的生命周期结束时,将执行方法 [onDestroyView]。后续可能重启另一个生命周期。

  • 第 6 行:方法 [onDestroyView] 会删除与该片段关联视图的所有链接。该链接将在片段的下一次生命周期中重新建立。 目前,我们需要将布尔值 [afterViews] 设置为 false,以表明与视图的关联已不存在;

我们将运行包含 5 个片段且邻接度为 2 的应用程序。修改内容如下所示:


    // 片段数量
  private final int FRAGMENTS_COUNT = 5;
  // 片段邻接性
  private final int OFF_SCREEN_PAGE_LIMIT=2;


  // 片段管理器
  private SectionsPagerAdapter mSectionsPagerAdapter;

   @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");

    ....

    // 片段偏移量
    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]方法。因此,这里可以看到一种情况,即在片段变得可见之后(第14行的片段1)调用了该方法;
  • 第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行:通过[visibleToUser]调用片段3中的方法[setUserVisibleHint],直至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] 中:


  // 片段数量
  private final int FRAGMENTS_COUNT = 5;
  // 片段邻接关系
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 个片段的生命周期已执行;
  • 片段 1 显示在第 18 行;

从标签页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] 的变化如下:


  // 片段数量
  private final int FRAGMENTS_COUNT = 5;
  // 片段邻接关系
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. 片段间通信

在上述架构中,我们有一个活动和 n 个片段。用户与各个片段进行交互。这些交互会改变应用程序的状态。这里所说的应用程序状态,是指其在整个生命周期中存储的所有信息。于是便产生了以下问题:

  • 当用户与片段 i 交互时,应用程序从状态 E1 切换到状态 E2;
  • 用户对片段 i 执行操作后,会显示片段 j;
  • 如何根据应用程序当前状态 E2 来更新片段 j;

从之前的示例中,我们知道如何更新片段 j。但该从何处获取应用程序的状态 E2 以便进行更新?

这个问题有多种解决方案。我们已经看到其中一种:片段 i 可以通过参数将应用程序的状态 E2 传递给片段 j。我们在创建片段时,曾在类 [MainActivity] 中遇到过这种方法:


      for (int i = 0; i < fragments.length; i++) {
        // 创建片段
        fragments[i] = new PlaceholderFragment_();
        // 可以向片段传递参数
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
}

但该方案在此处无法直接使用。因为当用户点击标签页 j 并显示片段 j 时,我们的代码并未被调用,此时仅执行系统代码。我们将在后续项目中探讨如何拦截标签页点击事件,但目前我们将采取另一种方案。

我们曾提到应用程序的状态:即应用程序随时间推移所管理的全部数据。在此,应用程序由一个活动和 n 个片段组成,这些片段均在应用程序启动时被实例化一次,其生命周期与应用程序一致。因此,这些元素中的任意一个或多个组合,都可作为存储应用程序状态的候选对象。 每个片段都可以通过方法 [Fragment.getActivity()] 访问创建它的 Activity。既然所有片段都能访问该 Activity,将应用程序的状态存储在其中似乎是理所当然的。

然而,方法 [Fragment.getActivity()] 的返回结果取决于其在生命周期中的调用时机。我们通过在类 [PlaceholderFragment] 中添加一些日志来说明这一点:


  // 更新片段
  public void update() {
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // 待执行的工作取决于访问编号
    if (numVisit > 1) {
      // 日志
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // 修改后的文本
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }

  // 日志的本地信息
  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行:方法[getInfos]显示应用程序状态的一部分;

我们以片段邻接度为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]方法确实返回了该活动;

当片段邻接度设为 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. 创建项目

我们将项目 [Exemple-07] 复制为 [Exemple-08]。为此,我们将遵循第 1.4 节中描述的将 [Exemple-02] 复制为 [Exemple-03] 的步骤。

1.10.2. 会话

在这个新项目中,我们希望片段能显示用户已查看的片段总数。为此,需要维护一个所有片段均可访问的计数器。 我们将封装片段共享数据的对象称为“会话”。这一术语源自Web开发,在Web开发中,同一用户请求的不同视图共享的数据会被放入一个会话中。将不同片段共享的信息封装在同一个对象中,能使代码更易于阅读。

[Session] 类的定义如下:

  

package exemples.android;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // 已访问片段数
  private int numVisit;

  // 获取器和设置器

  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 {

  ...

  // 会话注入
  @Bean(Session.class)
  protected Session session;

  // 片段数量
  private final int FRAGMENTS_COUNT = 5;
  // 片段邻接关系
  private final int OFF_SCREEN_PAGE_LIMIT = 2;

    @AfterInject
  protected void afterInject(){
    Log.d("MainActivity", "afterInject");

    // 会话初始化
    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 {

....

  // 会话
  protected Session session;

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // 父节点
    super.setUserVisibleHint(isVisibleToUser);
    // 内存
    this.isVisibleToUser = isVisibleToUser;
    // 日志
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // 访问次数
    if (isVisibleToUser) {
      // 更新片段
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // 片段将被隐藏
      updateDone = false;
    }
  }

  // 更新片段
  public void update() {
    // 日志
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // 会话
    if (session == null) {
      session = ((MainActivity) getActivity()).getSession();
    }
    // 访问次数递增
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // 文本已修改
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }
  • 第7行:会话;
  • 第35-37行:我们知道当进入方法[update]时,方法[getActivity]确实返回了活动。我们借此机会获取会话并将其保存在本地(第36行);
  • 第39-41行:为了递增访问次数,我们将从会话中获取该值。其实也可以将这段代码放在[setUserVisibleHint]方法的第19行之后,因为我们知道此时[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行:可见该活动的[afterInject]方法在[afterViews]方法之前执行;

欢迎读者测试这款新应用程序。

1.10.5. 禁用滑动(Swipe)功能

在前一个应用程序中,当用鼠标在 Android 模拟器上向左或向右滑动时,当前视图会根据情况切换为右侧或左侧视图。这种默认行为并不总是理想的。我们将学习如何禁用视图滑动(swipe)。

让我们回到主视图 XML:

  

在视图的代码 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 行指代管理该活动页面的类。该类可在活动 [MainActivity] 中找到:


import android.support.v4.view.ViewPager;
...

@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

  // 片段管理器
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // 片段容器
  @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 {

  // 控制滑动
  private boolean isSwipeEnabled;

  // 构造函数
  public MyPager(Context context) {
    super(context);
  }

  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  // 需重写的方法以管理滑动
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // 允许滑动吗?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // 允许滑动吗?
    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] 以及主活动 [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 {

  // 片段管理器
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // 片段容器
  @ViewById(R.id.container)
  protected MyPager mViewPager;

  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
...
    // 片段容器与片段管理器相关联
    // 即片段容器中的第 i 个片段是片段管理器提供的第 i 个片段
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // 禁止在片段之间滑动
    mViewPager.setSwipeEnabled(false);
    // 标签栏也与片段容器相关联
...
  • 第12行:页面管理器的类型现为[MyPager];
  • 第23行:是否禁用手势滑动。

请测试此新版本。启用或禁用滑动功能,并观察当您用鼠标向左或向右拖动视图时,其行为有何差异。在所有后续应用中,滑动功能将被禁用。我们将不再对此进行提醒。

1.10.6. 禁用片段间的滚动

接下来我们继续改进标签页管理器。当从标签页 1 切换到标签页 4 时,中间的两个标签页 2 和 3 会滚动显示。在 Android 术语中,这被称为 smoothScrolling。 如果标签页数量较多,这种行为可能会令人困扰。通过在片段管理器 [MyPager] 中添加以下代码,可以禁用此行为:


// 控制片段间的滑动
  private boolean isSwipeEnabled;
  // 控制滚动
  private boolean isScrollingEnabled;

 ...
  // 滚动
  @Override
  public void setCurrentItem(int position){
    super.setCurrentItem(position,isScrollingEnabled);
  }

  // 设置器
...

  public void setScrollingEnabled(boolean scrollingEnabled) {
    isScrollingEnabled = scrollingEnabled;
  }

由于标签管理器已与片段管理器 [MyPager] 关联,因此当点击第 i 个标签时,片段管理器会通过上述方法 [setCurrentItem](第 9 行)在片段容器中显示第 i 个片段。 [position] 是待显示片段的编号;

  • 第 10 行:调用父类的 [setCurrentItem] 方法。 [false] 的第二个参数要求旧片段与新片段之间立即过渡(无滚动),而 [true] 则要求通过 scrolling 进行过渡。 此处,第二个参数是第4行字段的值,开发者可通过第16-18行的方法设置该字段;

若要禁用滚动,[MainActivity]类的代码如下:


...
    // 片段偏移量
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

    // 禁止片段间滑动
    mViewPager.setSwipeEnabled(false);

    // 不滚动
    mViewPager.setScrollingEnabled(false);
...

再次运行项目,并确认例如在第1和第4选项卡之间不再出现scrolling。在后续操作中,我们将始终禁用滚动功能,此处不再赘述。

1.10.7. 一个新片段

在本示例中,所有片段均为同一类型 [PlaceHolderFragment]。现在我们将学习如何创建一个新片段并将其显示出来。

首先,将项目 [Exemple-04] 中的视图 [vue1.xml] 复制到项目 [Exemple-09] [1] 中:

 
  • 在 [1] 中,视图 [vue1.xml];
  • 在 [3] 中,视图因 [res/values/strings.xml] 文件中缺少文本而出现错误;

在 [2] 中,从项目 [Exemple-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>
  <!-- 视图 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 {

  // 视觉界面元素
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  // 事件管理器
  @Click(R.id.buttonValider)
  protected void doValider() {
    // 显示输入的名称
    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 {

    // 片段
    private Fragment[] fragments;
    // 片段编号
    private static final String ARG_SECTION_NUMBER = "section_number";

    // 构造函数
    public SectionsPagerAdapter(FragmentManager fm) {
      // 父级
      super(fm);
      // 初始化片段数组
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        // 创建片段
        fragments[i] = new PlaceholderFragment_();
        // 可以向片段传递参数
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
      // 一个片段的 +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }

 ...
  }
  • 第 13 行:包含 [FRAGMENTS_COUNT] 片段:[FRAGMENTS_COUNT-1] 片段(类型为 [PlaceholderFragment],见第 14-21 行)以及一个类型为 [Vue1Fragment_] 的片段, 第23行(注意下划线);

编译并运行项目 [Exemple-09]。第 5 个选项卡应有所不同:

1.10.8. 让所有片段都从同一个抽象类派生

新片段 [Vue1Fragment] 在显示时也需要进行更新。为此,我们需要编写一段与为片段 [PlaceholderFragment] 创建的代码类似的代码。 为避免重复,我们将可提取的部分归纳到一个抽象类中,应用程序中的所有片段都将继承该类。

为此,我们创建一个新项目。

1.11. 示例-10:让所有片段从一个抽象类派生

1.11.1. 创建项目

将项目 [Exemple-09] 复制为 [Exemple-10]:

1.11.2. 调试模式管理

我们在项目中添加了显示或隐藏调试模式日志的功能。为此,我们在类 [MainActivity] 中添加了一个静态常量:


  // 调试模式
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 boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private String className;

  // 子类可访问的数据
  protected boolean afterViewsDone = false;
  protected boolean isDebugEnabled = true;

  // 活动
  protected MainActivity activity;

  // 会话
  protected Session session;

  // 构造函数
  public AbstractFragment() {
    // 初始化
    isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
    className = getClass().getSimpleName();
    // 日志
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // 父级
    super.setUserVisibleHint(isVisibleToUser);
    ...
  }

  @Override
  public void onDestroyView() {
    // 父级
    super.onDestroyView();
    ...
  }

  @Override
  public void onResume() {
    // 父级
    super.onResume();
    ...
  }

  // 本地信息
  protected String getParentInfos() {
    return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
  }

  // 更新片段
  protected void update() {
    ...
    // 要求子类进行更新
    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) {
    // 父级
    super.setUserVisibleHint(isVisibleToUser);
    // 内存
    this.isVisibleToUser = isVisibleToUser;
    // 日志
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
    }
    // 片段何时可见
    if (isVisibleToUser) {
      // 更新片段
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // 离开片段
      updateDone = false;
    }
  }

  @Override
  public void onDestroyView() {
    // 父级
    super.onDestroyView();
    // 更新指示器
    afterViewsDone = false;
    // 日志
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
    }
  }

  @Override
  public void onResume() {
    // 父级
    super.onResume();
    // 日志
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
    }
    if (isVisibleToUser) {
      // 更新
      if (!updateDone) {
        update();
        updateDone = true;
      }
    }
  }

方法 [update] 如下:


  // 更新片段
  protected void update() {
    // 获取活动和会话
    if (activity == null) {
      Activity activity = getActivity();
      if (activity != null) {
        this.activity = (MainActivity) activity;
        this.session = this.activity.getSession();
      }
    }
    // 请求子类进行更新
    updateFragment();
}

根据上述代码,当片段的 [update] 方法执行时,该片段处于可见状态。 这一点很重要,因为这意味着方法 [Fragment.getActivity] 此时会返回应用程序活动的引用(参见第 1.10.8 节),从而可以访问会话。

  • 第 4-10 行:如果尚未初始化,则初始化活动和会话;
  • 第 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.*;

// 片段是由片段容器显示的视图
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {

  // 视觉界面的组件
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;

  // 数据
  private boolean initDone;

  // 数据
  private String text;
  private int numVisit;

  // 片段编号
  private static final String ARG_SECTION_NUMBER = "section_number";

  // 构造函数
  public PlaceholderFragment() {
    super();
    // 日志
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", "constructor");
    }
  }


  @AfterViews
  protected void afterViews() {
    // 内存
    afterViewsDone = true;
 ...
  }

  // 更新片段
  public void updateFragment() {
  ...
  }

}
  • 第 10 行:类 [PlaceholderFragment] 继承自类 [AbstractFragment]。采用此架构,编写片段包括:
    • 编写方法 [@AfterViews],用于在片段的生命周期初始阶段对其进行初始化,或者在之前存在 [onDestroyView] 时将其重置。第 39 行是必需的,用于正确管理片段的生命周期;
    • 编写方法 [updateFragment],该方法将在片段显示前对其进行更新。此方法可使用其父类的会话;
    • 编写片段的事件处理程序。这是我们在未来项目中将要完成的工作;

方法 [@AfterViews] 和 [updateFragment] 与上一个项目中的功能保持一致:


@AfterViews
  protected void afterViews() {
    // 存储器
    afterViewsDone = true;
    // 日志
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    if (!initDone) {
      // 初始文本
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // 初始化完成
      initDone = true;
    }
    // 显示当前文本
    textViewInfo.setText(text);
  }

  // 更新片段
  public void updateFragment() {
    // 日志
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    // 访问次数递增
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // 文本已修改
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }

  // 日志本地信息
  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 {

  // 视觉界面元素
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  // 数据
  private int numVisit;

  @AfterViews
  protected void afterViews() {
    // 内存
    afterViewsDone = true;
    // 日志
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
    }
  }

  // 事件管理器
  @Click(R.id.buttonValider)
  protected void doValider() {
    // 显示输入的名称
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  // 日志的本地信息
  protected String getLocalInfos() {
    return String.format("numVisit=%s", numVisit);
  }

  // 更新片段
  @Override
  protected void updateFragment() {
    // 访问次数递增
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // 显示访问次数
    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 行);

欢迎读者测试这个新项目。

在今后的所有项目中,我们将沿用这一架构:

  • 一个活动和 n 个片段;
  • 所有片段均继承自类 [AbstractFragment];
  • 片段之间以及片段与活动之间共享的数据放置在类 [Session] 中;

1.11.6. 标签页与片段的关联

在管理标签页的类 [MainActivity] 中写道:


// 标签栏也与片段容器相关联
// 即第 i 个标签页显示容器中的第 i 个片段
tabLayout.setupWithViewPager(mViewPager);

第 3 行将标签页管理器与片段容器关联起来。我们已经看到了这种关联的一个结果:当用户点击第 i 个标签页时,片段容器会显示第 i 个片段。但我们尚未看到反向情况:当要求片段容器显示第 i 个片段时,第 i 个标签页会自动被选中。

为了演示这种行为,我们将向当前菜单中添加选项 [Fragment 1, Fragment 2, ...]。当用户点击选项 [Fragment 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>
  <!-- 视图 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>
  <!-- 菜单 -->
  <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) {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // 处理菜单选项
    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;
      }
    }
    // 已处理的项目
    return true;
  }

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // 切换显示的片段
      mViewPager.setCurrentItem(i);
    }
  }
  • 第 2 行:当点击其中一个菜单选项时,会调用方法 [onOptionsItemSelected];
  • 第 8 行:获取被点击选项的标识符;
  • 第 9-36 行:通过 switch 处理各种情况;
  • 第16-36行:点击[Fragment i]选项会调用第41-45行的[showFragment(i-1)]方法;
  • 第43行:请求片段容器显示所请求的片段;
  • 第42行:事先验证是否可以执行(条件1)以及是否必要(条件2);

欢迎读者测试此新版本。可以发现,当请求显示第 i 个片段时,该片段确实被显示出来,且第 i 个标签页本身也被选中。

既然我们已经了解了标签页与片段的关联机制,接下来我们将探讨另一种情况:即标签页管理与片段管理分离的情形。例如,当标签页数量少于片段数量时便是如此。为了演示这种新的使用场景,我们将构建一个新项目。

1.12. 示例-11:标签与片段分离

1.12.1. 创建项目

将项目 [Exemple-10] 复制为 [Exemple-11]:

1.12.2. 目标

新应用程序将包含两个标签页:

  • 第一个标签页将始终显示片段 [Vue1];
  • 第二个标签页将显示从菜单中选定的片段;

Image

  • 在 [1] 中,显示片段 [Vue1];
  • 在 [2] 中,显示用户选择的 [PlaceholderFragment] 类型片段;
  • 在 [3] 中,继续统计访问次数;

1.12.3. 会话

  

新会话将如下所示:


package exemples.android;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // 已访问片段数
  private int numVisit;
  // 在第二个标签页中显示的 [PlaceholderFragment] 类型片段编号
  private int numFragment;

  // 获取器和设置器
...
}
  • 第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 行中的四个片段之一。第 5 个片段是 [Vue1Fragment],该片段将始终显示在第 1 个标签页中。

1.12.5. 类 [MainActivity]

类 [MainActivity] 现在必须管理标签页及其间的导航,这是它此前未曾做过的。其代码演变如下:


  // 标签页管理器
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
...
@AfterViews
  protected void afterViews() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
    ...

    // 无滚动
    mViewPager.setScrollingEnabled(false);

    // 显示视图1
    mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

    // 初始时只有一个标签页
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);

    // 事件管理器
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // 已选中一个标签页 - 通过片段容器更改显示的片段
        ...
      }

      @Override
      public void onTabUnselected(TabLayout.Tab tab) {

      }

      @Override
      public void onTabReselected(TabLayout.Tab tab) {

      }
    });

...

}
  • 第 17 行:片段容器显示的第一个片段将是 [Vue1Fragment]。根据设计,这将是容器中的最后一个片段;
  • 第20-22行:由于未将标签页与片段容器建立关联,我们需要自行管理标签页。起初,第3行中的标签栏[tabLayout]没有任何标签页;
  • 第 20 行:创建第一个标签;
  • 第21行:为其设置标题。在之前的示例中,标签页的标题即为片段的标题。现在这种情况不再存在。因此,我们从片段管理器中移除了方法 [getPageTitle]。我们不再需要它:

    // 可选 - 为管理的片段命名
    @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");
        }
        // 已选中一个标签页 - 通过片段容器更改显示的片段
        // 标签页位置
        int position = tab.getPosition();
        // 要显示的片段编号
        int numFragment;
        switch (position) {
          case 0:
            // 片段编号 [Vue1Fragment]
            numFragment = FRAGMENTS_COUNT - 1;
            break;
          default:
            // 片段编号 [PlaceholderFragment]
            numFragment = session.getNumFragment();
        }
        // 片段显示
        mViewPager.setCurrentItem(numFragment);
}
  • 第 8 行:获取被点击的标签页位置。此处将返回数字 0 或 1;
  • 第12-15行:若点击的是第一个标签页,则准备显示片段[Vue1Fragment];
  • 第16-18行:在其他情况下(点击了第2个标签页),准备重新显示上次选择第2个标签页时显示的片段。该片段的编号当时已被存入应用程序会话中;
  • 第21行:请求片段容器显示所需的片段;

现在来看菜单选项的管理(仍在 [MainActivity] 中):


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // 处理菜单选项
    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;
      }
    }
    // 处理项
    return true;
}
  • 第16-31行:管理菜单的4个选项。每个管理器都会调用方法[showFragment],并传入要显示的片段编号;

方法 [showFragment] 如下:


  // 第 2 个选项卡
  private TabLayout.Tab tab2 = null;

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // 如果第2个选项卡还不存在,则创建它
      if (tab2 == null) {
        tab2 = tabLayout.newTab();
        tabLayout.addTab(tab2);
      }
      // 设置第二个选项卡的标题
      tab2.setText(String.format("Fragment n° %s", (i + 1)));
      // 更改显示的片段
      mViewPager.setCurrentItem(i);
      // 将显示片段的编号保存到会话中
      session.setNumFragment(i);
      // 选中第2个标签页——若该标签页已被选中则不执行任何操作
      tab2.select();
    }
}
  • 需注意,应用程序启动时仅有一个标签页;
  • 第2行:初始时对第2个标签页的引用,即null
  • 第 5 行:显示条件与上一版本相比没有变化;
  • 第7-10行:如果第2个标签页尚不存在,则创建该标签页(第8行)并将其添加到标签栏中(第9行);
  • 第12行:将要显示的片段编号(编号从1开始)填入第二个标签页的标题中;
  • 第14行:显示所需的片段;
  • 第16行:将其编号存入会话中;
  • 第18行:选中第2号标签页。若该标签页已被选中,则不会发生任何操作:方法[onTabSelected]将不会被执行。若该标签页尚未被选中,则方法[onTabSelected]将被触发。 该方法随后会要求片段容器显示第14行中已显示的片段。在方法[onTabSelected]中进行一个简单的测试即可避免这种情况:

        // 仅在必要时显示片段
        if (numFragment != mViewPager.getCurrentItem()) {
          mViewPager.setCurrentItem(numFragment);
}

欢迎读者测试此新版本。

1.12.6. 改进

现在,我们已经充分理解了片段、其生命周期、片段邻接性概念以及它们与标签栏的关系。此外,我们还拥有一个稳健的架构,该架构刚刚通过了示例 11 的测试:

  • 一个活动和 n 个片段;
  • 所有片段均继承自类 [AbstractFragment];
  • 片段之间以及片段与活动之间共享的数据被放置在类 [Session] 中;

我们将通过在新的项目中添加一个接口,来明确活动与片段之间的关系。

1.13. 示例-12:定义活动与片段之间的关系

在此示例中,我们希望定义活动与片段之间的基本关系。为此,我们将使用:

  • 一个接口 [IMainActivity],用于定义片段可以向活动请求的内容;
  • 一个抽象类 [AbstractFragment],用于定义所有片段应具备的状态和方法;

1.13.1. 创建项目

我们按照第1.4节的步骤,将项目[Exemple-11]复制为[Exemple-12]。结果如下:

1.13.2. [IMainActivity] 接口

从前面的示例可以看出,片段需要访问由活动实例化的会话。此外,虽然这些示例中未体现,但可以预见:片段的事件处理程序有时会以视图切换结束。我们将要求活动执行此切换。因此,接口 [IMainActivity] 可能如下所示:

  

package exemples.android;

public interface IMainActivity {

  // 访问会话
  Session getSession();

  // 切换视图
  void navigateToView(int position);

  // 调试模式
  boolean IS_DEBUG_ENABLED = true;
}

第 12 行中,请注意出现了一个此前位于类 [MainActivity] 中的常量。我们希望降低片段与活动之间的耦合度,将其简化为 [AbstractFragment] 与 [IMainActivity] 之间的耦合。 届时,该活动的名称将不再是 [MainActivity]。由于常量 [IS_DEBUG_ENABLED] 在片段中被使用,因此将其移至接口 [IMainActivity] 中。

1.13.3. 抽象类 [AbstractFragment]

抽象类 [AbstractFragment] 的变更非常少:


  // 数据  可供子类访问
  protected boolean afterViewsDone = false;
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;

  // 活动
  protected IMainActivity mainActivity;
  protected Activity activity;

...
  // 更新片段
  protected void update() {
    // 获取活动和会话
    if (mainActivity == null) {
      this.activity = getActivity();
      if (this.activity != null) {
        this.mainActivity = (IMainActivity) activity;
        this.session = this.mainActivity.getSession();
      }
    }
    // 请求子类进行更新
    updateFragment();
}
  • 第 6 行和第 7 行:维护了两种针对该活动的引用:
    • 第 6 行:指向实现 [IMainActivity] 接口的活动;
    • 第 7 行:指向继承自 Android 类 [Activity] 的 Activity 的引用。所有 Activity 都是如此;

这两个引用当然指向同一个对象。但该对象被视为两种不同的类型。这将避免我们在运行时进行类型转换;

  • 第 14 行:通过方法 [getActivity] 获取对该 Activity 的引用;
  • 第 15 行:如果该引用不为空,则可以访问会话;
  • 第16-17行:将该活动作为实现[IMainActivity]接口的对象与会话一同保存;

1.13.4. 修改片段管理器

[MainActivity] 类中的片段管理器 [SectionsPagerAdapter] 仅在一点上进行了修改:它不再管理 [Fragment] 类型的片段,而是管理 [AbstractFragment] 类型的片段:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // 片段
    private AbstractFragment[] fragments;
    // 片段编号
    private static final String ARG_SECTION_NUMBER = "section_number";

    // 构造函数
    public SectionsPagerAdapter(FragmentManager fm) {
      // 父类
      super(fm);
      // 片段数组的初始化
      fragments = new AbstractFragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        ...
      }
      // 一个+片段
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }

    // 片段编号位置
    @Override
    public AbstractFragment getItem(int position) {
      ...
    }

    // 返回管理的片段数量
    @Override
    public int getCount() {
      ...
    }
}

1.13.5. 修改类 [MainActivity]

类 [MainActivity] 必须实现接口 [IMainActivity]:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{

...
  // 注入会话
  @Bean(Session.class)
  protected Session session;
...
  // 获取会话
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position) {
    // 显示位置视图
    if(mViewPager.getCurrentItem()!=position){
      // 显示片段
      mViewPager.setCurrentItem(position);
    }
  }

  • 第 10-12 行:方法 [getSession] 已存在;
  • 第 15-22 行:方法 [navigateToView] 用于显示片段编号 [position];
  • 第 17 行:检查是否有操作需要执行;
  • 第 19 行:显示片段编号 [position];

此时,请运行该应用程序。它应该可以正常运行。

1.13.6. 修改 [MainActivity] 中片段的显示

目前,类 [MainActivity] 通过以下语句显示一个片段:


    // 显示视图1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

由于方法 [navigateToView] 执行相同操作,因此将此类语句在所有位置(共 2 处)替换为:

navigateToView(...);

随后运行应用程序。它应该仍然可以正常运行。

1.13.7. 结论

从现在起,我们将始终采用以下架构:

  • 一个实现 [IMainActivity] 接口的活动;
  • 扩展 [AbstractFragment] 类的片段,这要求它们实现 [updateFragment] 方法。 这些片段还必须包含一个 [@AfterViews] 方法,其中将布尔值 [afterViewsDone] 设置为 true
  • 一个封装了片段与活动之间共享数据的会话;

1.14. 示例-13:包含片段的示例-05

在项目 [Exemple-05] 中,我们引入了视图间的导航。当时这属于活动间的导航:1 个视图 = 1 个活动。在此,我们计划仅使用一个活动,并包含多个 [AbstractFragment] 类型的视图。

1.14.1. 创建项目

我们按照第1.4节的步骤,将之前的项目[Exemple-12]复制为[Exemple-13]。结果如下:

1.14.2. 项目结构化

我们将开始使用包来组织代码。目前,我们可以区分两个不同的领域:

  • 活动管理;
  • 片段管理;

为此,我们创建了两个包 [exemples.android.activity] 和 [exemples.android.fragments]:

 

同样地,我们创建了 [exemples.android.fragments] 包:

在 [8] 中,我们创建了一个名为 [architecture] 的第三个包,并将 [IMainActivity, AbstractFragment, Session, MyPager] 实体放入其中,这些实体是我们应用程序架构的基础元素。这样做是为了提醒我们,我们已经做出了明确的架构选择。 接下来,请按照 [9] 中的指示移动项目中的现有元素。每次移动后,都需点击 [Refactor] 按钮进行确认。

此时,请编译应用程序。在 [MainActivity] 中出现以下错误:

 

在将类移动到包的过程中,Android Studio 已对应用程序代码进行了必要的修改(例如第 18-21 行)。 第 15 行和第 17 行涉及的类并未被移动。它们是由 Android Annotations 库生成的。对于这些类,需要手动修改 imports。因此,这些行将变为:

 

完成上述操作后,编译错误已消除。运行应用程序,此时会出现以下错误:

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 行将指定活动设为 [exemples.android.MainActivity_]。但由于该活动已迁移至 [activity] 包中,因此第 12 行现在应修改为:


      android:name=".activity.MainActivity_"

请注意 [activity] 前面的 .。同样,Android Studio 无法更新清单文件,因为该文件引用了一个 Android Annotations 类,而该类并未被移动。因此,使用 AA 库会带来一些不便。

1.14.3. 清理项目

在新项目中:

  • 不再有标签页、悬浮按钮和菜单;
  • [PlaceholderFragment]片段已消失。应用程序将管理两个片段:已有的[Vue1Fragment]以及需要创建的[Vue2Fragment];
  • 会话已发生变更;

1.14.3.1. 清理片段

删除类 [PlaceHolderFragment] [1]:

 

同时删除与该片段 [2] 关联的视图 [res / layout / fragment_main.xml]。

1.14.3.2. 清理会话

当前会话如下:


package exemples.android.architecture;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // 已访问片段数
  private int numVisit;
  // 在第二个标签页中显示的 [PlaceholderFragment] 类型片段编号
  private int numFragment;

  // 获取器和设置器

  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 {

  // 视觉界面的元素
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  @AfterViews
  protected void afterViews() {
    // 内存
    afterViewsDone = true;
    // 日志
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // 事件管理器
  @Click(R.id.buttonValider)
  protected void doValider() {
    // 显示输入的名称
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }


  // 更新片段
  @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] 中,删除所有与选项卡、浮动按钮、工具栏和菜单相关的代码。查找这些引用最简单的方法是删除它们的声明:


  // 标签页管理器
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // 浮动按钮
  @ViewById(R.id.fab)
protected FloatingActionButton fab;

并重新编译应用程序。出现错误的行正是引用了已删除元素的行。请删除所有这些行。此外,请修改片段管理器,使其不再引用我们已删除的片段 [PlaceholderFragment]:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // 片段
    private AbstractFragment[] fragments;

    // 构造函数
    public SectionsPagerAdapter(FragmentManager fm) {
      // 父级
      super(fm);
    }

    // 片段编号位置
    @Override
    public AbstractFragment getItem(int position) {
      // 日志
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }

    // 返回管理的片段数量
    @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 {

  // 片段容器
  @ViewById(R.id.container)
  protected MyPager mViewPager;
// 工具栏
@ViewById(R.id.toolbar)
protected Toolbar toolbar;

  // 会话注入
  @Bean(Session.class)
  protected Session session;

  // 片段数量
  private final int FRAGMENTS_COUNT = 5;
  // 片段邻接关系
  private final int OFF_SCREEN_PAGE_LIMIT = 2;

  // 调试模式
  public static final boolean IS_DEBUG_ENABLED = true;

  // 片段管理器
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // 构造函数
  public MainActivity() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "constructor");
    }
  }

  @AfterViews
  protected void afterViews() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }

    // 工具栏——应用程序名称显示在此处
    setSupportActionBar(toolbar);

    // 片段管理器
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // 片段容器与片段管理器相关联
    // 即片段容器中的第 i 个片段是片段管理器提供的第 i 个片段
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // 片段偏移量
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

    // 禁止在片段之间滑动
    mViewPager.setSwipeEnabled(false);

    // 禁用滚动
    mViewPager.setScrollingEnabled(false);

    // 视图1
    navigateToView(FRAGMENTS_COUNT - 1);

  }

  @AfterInject
  protected void afterInject() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
  }

  // 获取会话
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position) {
    // 显示视图位置
    if (mViewPager.getCurrentItem() != position) {
      // 显示片段
      mViewPager.setCurrentItem(position);
    }
  }

  // 片段管理器
  // 向其请求在主视图中显示的片段
  // 必须定义方法 [getItem] 和 [getCount] ——其余方法为可选
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // 片段
    private AbstractFragment[] fragments;

    // 构造函数
    public SectionsPagerAdapter(FragmentManager fm) {
      // 父级
      super(fm);
    }

    // 片段编号位置
    @Override
    public AbstractFragment getItem(int position) {
      // 日志
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }

    // 返回管理的片段数量
    @Override
    public int getCount() {
      return fragments.length;
    }
  }
}

还有一些修改需要进行:

  • 删除第 31 行,该行已无存在必要;
  • 第 33 行:将片段邻接度设为 1;
  • 第 76 行:导航至视图 0。该视图将首先显示;
  • 第108行:使用片段[Vue1Fragment_]初始化数组:

    // 片段
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};

因此我们只有一个片段。运行应用程序。您应得到以下结果:

Image

按钮 [Valider] 应当可以正常工作。

1.14.4. 创建片段及相关视图

该应用程序将包含两个视图,即项目中的 [Exemple-05]。 当前项目中已存在视图 [vue1.xml]。现在我们将 [vue2.xml] 从 [Exemple-05] 复制到 [Exemple-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>
  <!-- 视图 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <!-- 视图 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>

我们将类 [Vue1Fragment] 复制为 [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.EFragment;

@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {

  @AfterViews
  protected void afterViews() {
    // 内存
    afterViewsDone = true;
    // 日志
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // 片段更新
  @Override
  protected void updateFragment() {
  }
}
  • 第 9 行:该片段关联到视图 [res / layout / vue2.xml];
  • 第 10 行:该类继承自抽象类 [AbstractFragment];
  • 第 12-20 行:必填方法 [@AfterViews];
  • 第23-25行:必须使用方法[updateFragment];

1.14.5. 片段的设置及其间的导航

该活动现在将管理两个片段。其类 [SectionsPagerAdapter] 更新如下:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // 片段
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};

    ...
}

接口 [IMainActivity] 通过其方法 [navigateToView] 实现视图间的导航。我们将处理片段 [Vue1Fragment] 中按钮 [Vue n° 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 {

  // 视觉界面元素
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  @AfterViews
  protected void afterViews() {
    // 内存
    afterViewsDone = true;
    // 日志
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // 事件处理程序 ----------------------------------
  @Click(R.id.buttonValider)
  protected void doValider() {
    // 显示输入的名称
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  @Click(R.id.buttonVue2)
  protected void showVue2() {
    mainActivity.navigateToView(1);
  }

  // 更新片段
  @Override
  protected void updateFragment() {
  }
}
  • 第 37-40 行:方法 [showVue2] 处理按钮 [Vue n° 2] 的“点击”事件;
  • 第 39 行:通过该活动的 [navigateToView] 方法进行导航。在此提醒,该活动已以以下形式存储在父类中:

  // 活动
protected IMainActivity mainActivity;

且当进入任何事件处理程序时,该活动已初始化。

  • 第34行:该语句使用了父类的变量[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() {
    // 内存
    afterViewsDone = true;
    // 日志
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // 事件处理程序 ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }

  // 片段更新
  @Override
  protected void updateFragment() {
  }
}
  • 第 24-27 行:方法 [showVue1] 处理按钮 [Vue n° 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 {
  // 名称
  private String nom;

  // 获取器和设置器
...
}
  • 第8行:输入的名称;

类 [MainActivity] 将按以下方式初始化会话:


  // 会话注入
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // 会话初始化
    session.setNom("");
}

1.14.7. 片段的最终编写

在片段 [Vue1Fragment] 中,我们修改了按钮点击处理程序 [Valider] 的代码:


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 {

  // 视觉界面元素
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

...
  // 事件处理程序 ----------------------------------

  @Click(R.id.buttonValider)
  protected void doValider() {
    // 保存输入的名称
    String nom = editTextNom.getText().toString();
    // 显示该名称
    Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
  }

  @Click(R.id.buttonVue2)
  protected void showVue2() {
    // 将输入的名称存入会话
    session.setNom(editTextNom.getText().toString());
    // 跳转至视图 2
    mainActivity.navigateToView(1);
  }

  // 更新片段
  @Override
  protected void updateFragment() {

  }
}
  • 第 31-37 行:处理对按钮 [Vue n° 2] 的点击;
  • 第 34 行:在跳转至视图 2 之前,将输入的名称存入会话中,以便新视图能够访问该名称;

视图 [Vue2Fragment] 的处理流程如下:


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 {

  // 视觉界面的组件
  @ViewById(R.id.textViewBonjour)
  protected TextView textViewBonjour;

  @AfterViews
  protected void afterViews() {
    // 内存
    afterViewsDone = true;
    // 日志
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // 事件处理程序 ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }

  // 片段更新
  @Override
  protected void updateFragment() {
    // 获取会话中输入的名称
    String nom = session.getNom();
    // 显示该名称
    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() {
    // 内存
    afterViewsDone = true;
    // 日志
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
}

该方法尚不完整。实际上,必须始终考虑片段在执行 [onDestroyView] 操作后被回收的情况。在此情况下,片段 1 的视图将被重新生成,而之前可能输入的名称将从视图中消失。 这是我们不希望看到的。目前,输入的名称仍会显示,因为片段 1 的邻接关系导致片段 [Vue1Fragment] 的生命周期仅执行一次。然而,最好还是预先考虑片段被回收的情况。

解决此问题有多种方法:

  • 可以利用方法 [update] 在每次显示片段时都会被系统性执行这一特性,来更新已输入的名称;
  • 也可以仅在方法 [@AfterViews] 被重新执行时进行此更新。我们将采用后一种方案;

我们将 [Vue1Fragment] 的代码修改如下:


    // 视觉界面元素
    @ViewById(R.id.editTextNom)
    protected EditText editTextNom;

    // 数据
    private String nom;

    @AfterViews
    protected void afterViews() {
        // 内存
        afterViewsDone = true;
        // 日志
        if (isDebugEnabled) {
            Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
        }
        // (重新)初始化显示文本
        editTextNom.setText(nom);
    }

    // 事件处理程序 ----------------------------------

...

    @Click(R.id.buttonVue2)
    protected void showVue2() {
        // 记录输入的名称,以便在片段被回收时能够找回
        nom = editTextNom.getText().toString();
        // 将输入的名称存入会话
        session.setNom(nom);
        // 跳转至视图 2
        activity.navigateToView(1);
}
  • 第 27 行:在即将从视图 1 切换至视图 2 时,保存已输入的名称;
  • 第 17 行:每次片段生命周期重新执行时,都会重新显示上次输入的名称;

对于片段 [Vue2Fragment],现有代码即可满足需求:


  // 视觉界面的组件
  @ViewById(R.id.textViewBonjour)
  protected TextView textViewBonjour;

  @AfterViews
  protected void afterViews() {
    // 内存
    afterViewsDone = true;
    // 日志
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // 更新片段
  @Override
  protected void updateFragment() {
    // 获取会话中输入的名称
    String nom = session.getNom();
    // 显示该名称
    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节的步骤,将之前的项目[Exemple-12]复制为[Exemple-13]。结果如下:

1.15.2. 视图 [vue1]

该应用程序仅包含一个视图 [vue1.xml]。因此,我们将删除另一个视图 [vue2.xml] 及其关联的片段:

 

编译应用程序。[MainActivity]中出现了错误:

 

在片段管理器 [SectionsPagerAdapter] 中修正以下第 4 行


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // 片段
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...

上述第 4 行应修改为:


    // 片段
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};

删除不再需要的导入语句 [Ctrl-Shift-O]。此时不应再出现编译错误。运行该项目:视图 1 应会显示。现在我们将对其进行修改。

我们将创建视图 [vue1.xml],用于生成随机数:

 

其组件如下:

Id
Type
Rôle
1
edtNbAleas
EditText
nombre de nombres aléatoires à générer dans l'intervalle entier [a,b]
2
edtA
EditText
valeur de a
2
edtB
EditText
valeur de b
4
btnExécuter
Button
lance la génération des nombres
5
ListView
lstReponses
liste des nombres générés dans l'ordre inverse de leur génération. On voit d'abord le dernier généré ;

其代码 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>
  <!-- 视图 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>
  <!-- 应用颜色 -->
  <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 {

  // 视觉界面的元素
  @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;

  // 命令响应列表
  private List<String> reponses = new ArrayList<>();
  // 列表视图适配器
  private ArrayAdapter<String> adapterReponses;

  // 输入字段
  private int nbAleas;
  private int a;
  private int b;

  @AfterViews
  protected void afterViews() {
    // 内存
    afterViewsDone = true;
    // 日志
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
    // 隐藏错误消息
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
  }

  @Click(R.id.btn_Executer)
  void doExecuter() {
    // 隐藏之前的任何错误消息
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // 验证输入数据的有效性
    if (!isPageValid()) {
      return;
    }
  }

  // 验证输入数据的有效性
  private boolean isPageValid() {
...
  }

  @Override
  protected void updateFragment() {
    // 日志
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
  }
}
  • 这里只有一个片段,其生命周期仅在应用程序启动时执行一次。 因此,方法 [@AfterViews](第 46-57 行)和 [udateFragment](第 75-81 行)仅在应用程序启动时执行一次;
  • 第 55-56 行:隐藏视图中的两个错误消息(如下所示)[1-2];
 
  • 第59-60行:点击按钮时执行的方法 [Exécuter];
  • 第71-73行:验证输入内容的有效性;

方法 [isPageValid] 如下:


  // 输入内容
  private int nbAleas;
  private int a;
  private int b;

...

// 验证输入数据的有效性
  private boolean isPageValid() {
    // 输入随机数个数
    nbAleas = 0;
    Boolean erreur;
    int nbErreurs = 0;
    try {
      nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
      erreur = (nbAleas < 1);
    } catch (Exception ex) {
      erreur = true;
    }
    // 错误?
    if (erreur) {
      nbErreurs++;
      txtErrorAleas.setVisibility(View.VISIBLE);
    }
    // 输入 a
    a = 0;
    erreur = false;
    try {
      a = Integer.parseInt(edtA.getText().toString());
    } catch (Exception ex) {
      erreur = true;
    }
    // 错误?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // 输入 b
    b = 0;
    erreur = false;
    try {
      b = Integer.parseInt(edtB.getText().toString());
      erreur = b < a;
    } catch (Exception ex) {
      erreur = true;
    }
    // 错误?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // 返回
    return (nbErreurs == 0);
  }

  • 第 2-4 行:这三个字段由方法 [isPageValid] 初始化。 此外,如果所有输入均有效,该方法将返回 true;否则返回 false。若存在无效输入,则显示相应的错误信息;

至此,应用程序已可运行。请通过输入错误数据来验证方法 [isPageValid] 的运行情况。

1.15.5. [métier] 层

  

[métier] 层具有以下 [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) {
        // 对象列表
        List<Object> réponses = new ArrayList<Object>();
        // 一些检查
        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"));
        }
        // 错误?
        if (réponses.size() != 0) {
            return réponses;
        }
        // 生成随机数
        Random random = new Random();
        for (int i = 0; i < n; i++) {
            // 以 1/3 的概率触发随机异常
            int nombre = random.nextInt(3);
            if (nombre == 0) {
                réponses.add(new AleaException("Exception aléatoire"));
            } else {
                // 否则返回介于两个边界值之间的随机数 [a,b]
                réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
            }
        }
        // 结果
        return réponses;
    }
}
  • 第9行:在类[Metier]上使用注解AA [@EBean],以便将其引用注入到层[Présentation]中。 属性 (scope = EBean.Scope.Singleton) 确保类 [Metier] 仅会被实例化一次。 因此,即使在 [Présentation] 层中多次注入,注入的始终是同一个引用;
  • 其余代码均为常规实现;

类 [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] 活动

  

[metier]

业务

视图

用户

该活动将实现 [métier] 层中的 [IMetier] 接口。因此,片段/视图将仅与该活动进行交互。

活动 [MainActivity] 已实现接口 [IMainActivity]。若要使其同时实现接口 [IMetier],可:

  • 将接口 [IMetier] 添加到该业务活动已实现的接口列表中;
  • 让接口 [IMainActivity] 自身继承接口 [IMetier]。我们将采用此方案;

接口 [IMainActivity] 变为如下形式:

  

package exemples.android.architecture;

import exemples.android.metier.IMetier;

public interface IMainActivity extends IMetier {

  // 访问会话
  Session getSession();

  // 切换视图
  void navigateToView(int position);

  // 调试模式
  public static final boolean IS_DEBUG_ENABLED = true;

}
  • 第 5 行:接口 [IMainActivity] 继承自接口 [IMetier]

类 [MainActivity] 的演变如下:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {

  ...

  // 会话注入
  @Bean(Session.class)
  protected Session session;

  // 业务注入
  @Bean(Metier.class)
  protected IMetier metier;

...
  // 实现IMetier --------------------------------------------------------------------
  @Override
  public List<Object> getAleas(int a, int b, int n) {
    return metier.getAleas(a, b, n);
}
  • 第 11-12 行:将 [métier] 层注入到业务活动中。 为此使用了注解 AA [@Bean],其参数是带有注解 AA [@EBean] 的类;
  • 第 2 行:该活动实现了接口 [IMainActivity],因此也实现了 [métier] 层中的接口 [IMetier];
  • 第16-19行:实现接口[IMetier]的唯一方法。仅将调用委托给[métier]层;

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 {

  // 视觉界面元素
  @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;

  // 命令响应列表
  private List<String> reponses = new ArrayList<>();
  // 列表视图适配器
  private ArrayAdapter<String> adapterReponses;

  // 数据输入
  private int nbAleas;
  private int a;
  private int b;

  @AfterViews
  protected void afterViews() {
   ...
  }

  @Click(R.id.btn_Executer)
  void doExecuter() {
  ...
  }

  // 验证输入数据的有效性
  private boolean isPageValid() {
   ...
  }

  @Override
  protected void updateFragment() {
    // 日志
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
    // 仅在应用程序启动时执行一次
    // 创建 ListView 适配器——为此,变量 [activity] 必须已初始化
    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 行使用的构造函数如下:


public ArrayAdapter (Context context, int resource, int textViewResourceId, List<T> objects)
  • [context] 是显示 [ListView] 的活动;
  • [resource] 是用于标识显示 [ListView] 元素的视图的整数。该视图可以具有任意复杂度。由开发人员根据需求构建该视图;
  • [textViewResourceId] 是用于标识视图 [resource] 中某个 [TextView] 组件的整数。该组件将负责显示相应的字符串;
  • [objects]:由 [ListView] 显示的对象列表。 使用对象的 [toString] 方法,在由 [textViewResourceId] 标识的 [TextView] 中,于由 [resource] 标识的视图中显示该对象。

开发者的任务是创建视图 [resource],该视图将显示 [ListView] 中的每个元素。 对于像这里这样仅需显示简单字符串的简单情况,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] 中。该字段并非总是具有值。 因此日志显示,当进入方法 [@AfterViews] 时,该字段尚未初始化,所以无法将第 69-70 行放入该方法中。 而在方法 [updateFragment] 中则可以,因为我们知道当该方法执行时,必然存在 [activity!=null]。此处的适配器关联的是第 37 行定义的数据源 [reponses];

方法 [doExecuter] 处理对按钮 [Exécuter] 的点击。其代码如下:


@Click(R.id.btn_Executer)
  void doExecuter() {
    // 清除之前的任何错误消息
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // 清除之前的响应
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    // 验证输入数据的有效性
    if (!isPageValid()) {
      return;
    }
    // 向活动请求随机数
    List<Object> data = mainActivity.getAleas(a, b, nbAleas);
    // 根据这些数据创建字符串列表
    for (Object o : data) {
      if (o instanceof Exception) {
        reponses.add(((Exception) o).getMessage());
      } else {
        reponses.add(o.toString());
      }
    }
    // 刷新列表视图
    adapterReponses.notifyDataSetChanged();
  }
  • 第7-8行:需要清空ListView。为此,清空数据源[reponses],并要求与ListView关联的适配器进行刷新;
  • 第10-12行:在执行请求的操作之前,需验证输入的值是否正确;
  • 第 14 行:向业务活动请求随机数列表。返回的对象列表中,每个对象的类型均为 [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,该服务将返回一个介于[a,b]范围内的随机数。我们将按以下顺序描述该应用程序:

服务器

  • 其 [métier] 层;
  • 其使用 Spring 实现的 [web / jSON] 服务;

客户端

  • 其 [DAO] 层。此处不包含 [métier] 层;

1.16.1. 服务器 [web / jSON]

我们希望构建以下架构:

1.16.1.1. 项目创建

我们将使用 Spring 生态系统构建 Web 服务 [http://spring.io/]。 我们访问 [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 {
    // 自 Android 的 Gradle 插件 0.11 起,必须使用 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行是针对IDE的Eclipse配置。我们将它们删除;
  • 第1-11行和第15行用于向我们的Gradle项目添加一个名为[spring-boot]的插件。 Spring Boot 是 Spring 生态系统中的一个项目。该插件定义了 Spring 最常用的依赖项版本。这样就无需明确指定这些版本(第 30 和 31 行)。此时,版本即为所用 Spring Boot 版本(第 3 行)所定义的版本;
  • 第 22-23 行:要使用的 Java 版本,此处为 1.8 版;
  • 第25-27行:用于下载依赖项的二进制仓库;
  • 第 26 行:指定 Maven 中央仓库。这是目前最大的开源二进制仓库;
  • 第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}")
  }
}

// 插件
apply plugin: 'java'
apply plugin: 'spring-boot'

// 项目二进制文件
jar {
  baseName = 'server-01'
  version = '0.0.1-SNAPSHOT'
}

// Java 版本
sourceCompatibility = 1.8
targetCompatibility = 1.8

// Maven 仓库
repositories {
  mavenLocal()
  mavenCentral()
}

// 依赖项
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 Web 应用 MVC 可能需要的依赖项。这意味着其中某些依赖项可能并不必要。Spring Boot 非常适合用于教程:

  • 它自带了我们可能需要的依赖项;
  • 它自带了一个嵌入式 Tomcat 服务器 [1],从而省去了将应用程序部署到外部 Web 服务器的步骤;

在 Spring 生态系统网站上,可以找到许多使用 Spring Boot 的示例 [http://spring.io/guides]。

现在,我们按以下方式填写文件 [build.gradle]:


// Spring Boot
...
// 依赖项
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}

// 用于在本地 Maven 仓库中生成符合 Maven 标准的二进制文件的插件
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 {
      // 修改为指向您的仓库,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] 中添加以下代码:


// 生成包含所有依赖项的二进制文件
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. [métier] 层

  

[métier]层延续了前例中[métier]层的设计思路。其接口[IMetier]如下:


package exemples.android.server.metier;

public interface IMetier {
  // [a,b] 中的随机数
    int getAlea(int a, int b);
}
  • 第 5 行:在 [a,b] 中生成 1 个随机数的方法

实现该接口的 [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) {
    // 若干验证
    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);
    }
    // 生成结果
    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 {

  // 错误代码
  private int code;

  // 构造函数
  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;
  }

  // 获取器和设置器
....
}
  • 第 3 行:[AleaException] 继承自 [RuntimeException] 类。因此这是一个未受控的异常(无需使用 try/catch 进行处理);
  • 第 6 行:在类 [RuntimeException] 中添加了一个错误代码;

1.16.1.5. Web 服务 / jSON

 
  

Web 服务 / jSON 由 Spring MVC 实现。 Spring MVC 通过以下方式实现了所谓的 MVC 架构模式(模型 – 视图 – 控制器):

客户端请求的处理流程如下:

  1. 请求 - 请求的 URL 采用 http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... 格式[Dispatcher Servlet] 是 Spring 中的类,负责处理传入的 URL。 它将 URL “路由”到负责处理它的操作。这些操作是名为 [Contrôleurs] 的特定类的方法。此处 MVC 的 C 部分是字符串 [Dispatcher Servlet, Contrôleur, Action]。 如果未配置任何操作来处理传入的 URL,则 [Dispatcher Servlet] Servlet 将返回“未找到请求的 URL” (404 错误 NOT FOUND);
  1. 处理
  • 所选操作可利用 parami 参数,这些参数由 [Dispatcher Servlet] Servlet 传递而来。这些参数可能来自多个来源:
    • [/param1/param2/...] 的路径,
    • 来自 URL 的 [p1=v1&p2=v2] 参数
    • 浏览器随请求发送的参数;
  • 在处理用户请求时,该操作可能需要调用 [metier] 和 [2b] 层。一旦处理完客户端的请求,该操作可能会触发各种响应。一个典型的例子是:
    • 如果请求无法正确处理,则显示错误页面
    • 否则显示确认页面
  • 该操作会要求显示某个视图 [3]。该视图将显示被称为视图模型的数据。这就是 MVC 中的 M。 该操作将创建此模型 M [2c],并要求显示视图 V [3];
  1. 响应——选定的视图V使用操作生成的模型M来初始化响应HTML中的动态部分(该响应需发送给客户端),随后发送此响应。

对于 Web 服务 / jSON,上述架构稍作修改:

  • 在 [4a] 中,作为 Java 类的模型通过 jSON 库转换为字符串 jSON;
  • 在 [4b] 中,该字符串 jSON 被发送至浏览器;

关于将 Java 对象序列化为字符串 jSON 以及将字符串 jSON 反序列化为 Java 对象的示例,详见第 6.14 节的附录。

让我们回到应用程序的 [web] 层:

在我们的应用程序中,只有一个控制器:

  

Web 服务 / jSON 将向其客户端发送如下 [Response] 类型的响应:


package exemples.android.server.web;

import java.util.List;

public class Response<T> {

    // ----------------- 属性
    // 操作状态
    private int status;
    // 可能的错误消息
    private List<String> messages;
    // 响应正文
    private T body;

    // 构造函数
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // 获取器和设置器
...
}
  • 第 13 行:字段 [T body] 是客户端预期的响应。我们决定在此使用类型为 T 的通用响应,而非预期随机数类型 Integer。 我们希望能在其他场景中复用该类。在处理客户端请求时,服务器可能会遇到问题,此时问题详情将汇总在另外两个字段中;
    • 第 8 行:状态码(无错误时为 0);
    • 第 9 行:若状态为 !=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 {

  // 业务层
  @Autowired
  private IMetier metier;
  // 映射器JSON
  @Autowired
  private ObjectMapper mapper;

  // 随机数
  @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 {

    // 响应
    Response<Integer> response = new Response<>();
    // 使用业务层
    try {
      response.setBody(metier.getAlea(a, b));
      response.setStatus(0);
    } catch (AleaException e) {
      response.setStatus(e.getCode());
      response.setMessages(getMessagesFromException(e));
    }
    // 返回响应
    return mapper.writeValueAsString(response);
  }

  private List<String> getMessagesFromException(Throwable e) {
    // 消息列表
    List<String> messages = new ArrayList<String>();
    // 遍历异常堆栈
    Throwable th = e;
    while (th != null) {
      messages.add(e.getMessage());
      th = th.getCause();
    }
    // 返回结果
    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。当客户端希望在其 HTTP 请求中附加文档时,会使用 [POST] 方法;
    • [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 的组成部分,因此它们的类型为 StringString 向参数类型的转换可能会失败。此时 Spring MVC 会抛出异常。 总结如下:如果我通过浏览器请求 URL /100/200,第 30 行中的 getAlea 方法将使用整数参数 a=100、b=200 执行;
  • 第 36 行:向 [métier] 层请求一个介于 [a,b] 范围内的随机数。 需注意方法 [metier].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配置 ------------------------------------
  @Bean
  public DispatcherServlet dispatcherServlet() {
    DispatcherServlet servlet = new DispatcherServlet();
    return servlet;
  }

  @Bean
  public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
    return new ServletRegistrationBean(dispatcherServlet, "/*");
  }

  @Bean
  public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory("", 8080);
  }

  // 映射器 jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }

}
  • 第 12 行:告知 Spring 将在哪些包中找到它需要管理的两个组件:
    • 位于 [exemples.android.server.metier] 包中、标注为 [@Service] 的组件 [Metier];
    • 在 [exemples.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 行:定义了 Bean [dispatcherServlet]。这是 Spring 的预定义名称 MVC,用于定义应用程序 MVC 的前端控制器, 该对象负责处理所有客户端请求,并将其分发(故得此名)至 Spring 应用程序 MVC 中的各个 [@Controller];
  • 第 18 行:bean [dispatcherServlet] 是 Spring MVC 提供的 [DispatcherServlet] 类的实例;
  • 第22-25行:bean [servletRegistrationBean]用于定义应用程序接受哪些URL。第24行,接受所有URL;
  • 第27-30行:Bean [embeddedServletContainerFactory]用于定义项目依赖项中用于托管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) {
    // 应用程序执行
    SpringApplication.run(Config.class, args);
  }

}
  • 类 [Boot] 是一个可执行类(第 7-10 行);
  • 第 9 行:静态方法 [SpringApplication.run] 是 [spring Boot](第 4 行)的方法,该方法将启动应用程序。其第一个参数是配置该项目的 Java 类。 此处即为我们刚刚描述的 [Config] 类。第二个参数是传递给方法 [main](第 7 行)的参数数组;

可以通过多种方式启动 Web 应用程序,例如:

 

此时,控制台中会显示一些日志:

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

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

现在,我们打开浏览器,测试 Web 服务 /jSON 下的 URL:

每次我们都会得到一个类型为 [Response<Integer>] 的对象的表示形式 jSON。

现在,我们不使用标准浏览器,而是采用 Chrome 浏览器的 [Advanced Rest Client] 扩展程序 (参见附录第 6.13 节):

Image

  • 转换为 [1],即请求的 URL;
  • 转换为 [2],通过 GET 实现;
  • 转换为 [3],发送请求;

Image

  • 在 [4] 中,服务器响应的 HTTP 头部。请注意,该头部表明发送的文档是一个字符串 jSON;
  • 在 [5] 中,显示接收到的字符串 jSON;

1.16.1.8. 生成项目的可执行 jar 文件

在第 1.16.1.2 节中,我们展示了如何配置 Gradle 文件以生成包含所有依赖项的应用程序可执行文件。针对当前应用程序,该配置如下:


// 生成包含所有依赖项的二进制文件
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">
    <!-- 编码器默认被分配类型
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- 日志级别控制 -->
  <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 服务器的 Android 客户端 / jSON

Android客户端将采用以下架构:

客户端将包含两个组件:

  1. 一个 [Présentation] 层(视图+活动),与我们在示例 [Exemple-14] 中研究过的类似;
  2. [DAO] 层,该层面向我们之前研究过的 [web / jSON] 服务。

1.16.2.1. 创建项目

我们按照第1.4节的步骤,将之前的项目[Exemple-14]复制为[Exemple-15]。结果如下:

接下来,请读者创建以下项目。

1.16.2.2. Gradle 配置

 

文件 [build.gradle] 内容如下:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // 自 Android 的 Gradle 插件 0.11 起,必须使用 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'
    }
  }

  // 生成 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. [IDao] 接口(属于 [DAO] 层)

[DAO]层的接口如下:


package exemples.android.dao;

public interface IDao {

  // 随机数
  int getAlea(int a, int b);

  // Web 服务的 URL
  void setUrlServiceWebJson(String url);

  // 服务器响应的最大超时时间(毫秒)
  void setTimeout(int timeout);

  // 客户端在发送请求前的等待时间(毫秒)
  void setDelay(int delay);

}
  • 第6行:Web服务/jSON的方法,用于从该Web服务的[a,b]区间获取随机数;
  • 第 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 个随机数,取值范围为 [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
  • 第 12 行:[WebClient] 是一个接口,AA 库将通过其中添加的注解自行实现该接口。 该接口必须实现 Web 服务 / jSON 所暴露的对 URL 的调用:

  // 随机数
  @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行:一个AA注解,表明URL必须通过HTTP方法调用GET。 注释 [@Get] 的参数即为 Web 服务所期望的 URL 的格式。 只需将服务器控制器 [WebController] 中被调用方法的注解 [@RequestMapping](第 b 行)中的参数 [value] 复制过来即可。 大括号 {} 包围了 URL 的参数,这些参数需在第 16 行的方法参数中进行引用。 [@Path("a") int a]的语法使得该方法的参数[a]被赋值为URL中的{a}。 当 URL 的参数与方法参数名称相同(如本例所示)时,可以更简洁地写为 [@Path int a];

对于 HTTP POST 请求,调用方法的签名如下:


  @Post("/{a}/{b}")
  Response<Integer> getAlea(@Body T body, @Path("a") int a, @Path("b") int b);

注解 [@Body] 表示提交的值。该值将自动序列化为 jSON。在服务器端,签名如下:


  // 随机数
  @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 请求,且该请求的正文(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> {

    // ----------------- 属性
    // 操作状态
    private int status;
    // 可能的错误消息
    private List<String> messages;
    // 响应正文
    private T body;

    // 构造函数
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // 获取器和设置器
...
}

这正是服务器端已使用的 [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 {

  // 服务客户端 REST
  @RestService
  protected WebClient webClient;

  // 映射器jSON
  private ObjectMapper mapper = new ObjectMapper();
  // 请求执行前的等待延迟
  private int delay;

// 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],将其转换为 Bean AA,以便在其他地方进行注入;
  • 第19-20行:我们注入了之前描述的[WebClient]接口的实现。[@RestService]注解负责完成此注入;
  • 其余方法实现了 [IDao] 接口(第 27-46 行);

方法 [setTimeout]

方法 [setTimeout] 如下:


  @Override
  public void setTimeout(int timeout) {
    // 设置客户端请求的超时 REST
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
    // 构建 restTemplate
    RestTemplate restTemplate = new RestTemplate(factory);
    // 设置转换器 jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // 设置 Web 客户端的 restTemplate
    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 要求我们在 Web 客户端注释 [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) {
    // 服务运行
    Response<Integer> info;
    DaoException ex;
    try {
      // 等待
      waitSomeTime(delay);
      // 服务正在运行
      info = webClient.getAlea(a, b);
      int status = info.getStatus();
      if (status == 0) {
        // 返回结果
        return info.getBody();
      } else {
        // 记录异常
        ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
      }
    } catch (JsonProcessingException | RuntimeException e) {
      // 记录异常
      ex = new DaoException(e, 100);
    }
    // 抛出异常
    throw ex;
  }
...
  // 私有方法 -------------------
  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 {

  // 错误代码
  private int code;

  // 构造函数
  public DaoException() {
  }

  public DaoException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }

  public DaoException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }

  // 获取器和设置器
...
}
  • 第 6 行:异常 [DaoException] 是一个未受控异常;

方法 [setUrlServiceWebJson]

方法 [setUrlServiceWebJson] 如下:


  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // 设置服务 REST 的 URL
    webClient.setRootUrl(urlServiceWebJson);
}
  • 第 4 行:通过 [WebClient] 接口的 [setRootUrl] 方法设置 Web 服务的 URL。 正是因为该接口继承了 [RestClientRootUrl] 接口,所以才存在此方法;

1.16.2.5. [architecture] 包

[architecture] 包汇集了构成应用程序的结构化元素:

1.16.2.5.1. 接口 [IMainActivity]

接口 [IMainActivity] 列出了应用程序活动必须实现的方法:


package exemples.android.architecture;

import exemples.android.dao.IDao;

public interface IMainActivity extends IDao {

  // 访问会话
  Session getSession();

  // 视图切换
  void navigateToView(int position);

  // 等待
  void beginWaiting();

  void cancelWaiting();

  // 调试模式
  boolean IS_DEBUG_ENABLED = true;
  // 响应超时
  int TIMEOUT = 1000;
  // 片段邻接性
  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 {

  // 异常消息列表 - 版本 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // 创建包含异常堆栈中错误消息的列表
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }

  // 异常消息列表 - 版本 2
  static public String getMessagesForAlert(Throwable th) {
    // 构建待显示的文本
    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--;
    }
    // 结果
    return texte.toString();
  }

}
  • 第9-18行:创建包含在Throwable中的错误消息列表;
  • 第21-32行:基于前面的方法,利用获取到的错误消息列表构建Android警报消息中要显示的文本;
  • 第 27-28 行:对消息进行编号。编号最小的(1)对应初始异常,编号最大的对应异常堆栈中的最新异常;

1.16.2.5.3. 抽象类 [AbstractFragment]

[AbstractFragment] 类有两个作用:

  1. 确保在显示片段时,子类的 [updateFragments] 方法始终被调用且仅调用一次;
  2. 将子类中可提取的状态和方法进行提取;

正是基于第二个功能,我们才将待机图像的管理操作放入该类中:所有异步 Android 应用的片段都需要处理此类问题:


  // 处理等待
  protected void beginWaiting() {
    // 显示沙漏
    mainActivity.beginWaiting();
  }

  protected void cancelWaiting() {
    // 移除沙漏
    mainActivity.cancelWaiting();
}

1.16.2.6. 视图

1.16.2.6.1. 视图 [vue1.xml]
  

与前一个示例相比,视图 [vue1.xml] 的变化如下:

 
 
  • 在 [1] 中,用户必须在每次调用 Web 服务之前指定 Web 服务的 URL 以及超时时间 [2];
  • 在 [3] 中,响应会被计数;
  • 在 [4] 中,用户可以取消请求;
  • 在 [5] 中,当请求数字时会显示一个等待指示器。当所有数字均已接收或操作被取消时,该指示器将消失;

Image

  • 在 [6] 中,将对输入内容的有效性进行验证;

系统提示用户从示例中加载文件 [vue1.xml]。接下来,我们将列出新组件的标识符:

Image

Type
Id
1
EditText
edt_nbaleas
2
TextView
txt_errorNbAleas
3
EditText
edt_a
4
EditText
edt_b
5
TextView
txt_errorIntervalle
6
EditText
editTextUrlServiceWeb
7
TextView
textViewErreurUrl
8
EditText
editTextDelay
9
TextView
textViewErreurDelay
10
Button
btn_Executer
11
Button
btn_Annuler
12
TextView
txt_Reponses
13
ListView
lst_reponses

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

  // 视觉界面的元素
  @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;
...
  // 本地数据
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;

  @AfterViews
  void afterViews() {
    // 内存
    afterViewsDone=true;
    // 初始时无错误信息
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    textViewErreurDelay.setVisibility(View.INVISIBLE);
    // [Annuler] 按钮被隐藏
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);
    // 响应列表
    reponses = new ArrayList<>();
  }
...
  • 第24-49行:视图[vue1.xml](第20行)中组件的引用;
  • 第 55-69 行:当第 24-49 行的引用初始化后执行的方法 [@AfterViews];
  • 第 58 行:切勿遗漏——这是片段生命周期所必需的;
  • 第 60-63 行:隐藏错误消息;
  • 第65-66行:隐藏按钮[Annuler](第65行)并显示按钮[Exécuter](第66行)。需注意这两个按钮在物理上重叠;
  • 第 68 行:第 52 行的字段将包含由 ListView 响应生成的待显示字符串列表;

紧接在方法 [@AfterViews] 之后,将执行后续的方法 [updateFragment]:


  @Override
  protected void updateFragment() {
    // 创建答案列表适配器
    adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
}
  • 第4-5行:创建响应适配器ListView。将其存储在一个实例变量中,以便类中的其他方法可以使用;

点击按钮 [Exécuter] 将触发以下方法的执行:


// 输入内容
  private int nbAleas;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int delay;

  // 本地数据
  private int nbInfos;
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;
  private boolean hasBeenCanceled;

  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // 清除之前的回答
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // 将回答计数器重置为0
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // 验证输入的有效性
    if (!isPageValid()) {
      return;
    }
    // 初始化活动
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // 请求随机数
    for (int i = 0; i < nbAleas; i++) {
      getAlea(a, b);
    }
    // 开始等待
    beginWaiting();
  }

  @Background(id = "alea")
  void getAlea(int a, int b) {
    // 此处应尽量减少操作
    // 无论如何都不进行显示——这些操作应在UiThead中完成
    try {
      // 在 UiThread 中显示结果
      showInfo(mainActivity.getAlea(a, b));
    } catch (RuntimeException e) {
      // 在 UiThread 中显示异常
      showAlert(e);
    }
  }
  • 第17-18行:清除服务器上的先前响应列表。为此,在第17行,清空与ListView适配器关联的数据源[reponses];
  • 第19行:一个布尔值,用于判断用户是否已取消请求;
  • 第21-22行:显示响应数量为零的计数器;
  • 第24-26行:获取[2-6]表单中的输入内容并验证其有效性。若其中任何一项无效,则终止该方法(第25行),并让用户返回可视化界面;
  • 第28-29行:若所有输入数据均有效,则将Web服务的URL(第28行)以及每次调用服务前的等待时间(第29行)传递给该业务。 这些信息是 [DAO] 层所必需的,需要提醒的是,与该层进行通信的是该活动;
  • 第31-33行:随机数通过第39行的[getAlea]方法逐个请求;
  • 第38行:方法 [getAlea] 被标注为 AA [@Background],这意味着它将在另一个线程(执行流、 进程)中执行,该线程与视觉界面所在的线程不同。实际上,所有互联网调用都必须在与视觉界面不同的线程中执行。因此,在某个时刻,可能会存在多个线程:
    • 一个用于显示 UI 视觉界面(用户界面)并管理其事件的线程,
    • 以及若干 [nbAleas] 线程,每个线程都会向 Web 服务请求一个随机数。 这些线程以异步方式启动:UI 线程会启动一个 [getAlea] 线程(第 32 行),该线程向 Web 服务请求随机数,且不等待其完成。完成后将通过事件通知该线程。 因此,[nbAleas] 线程将并行启动。可以配置应用程序使其每次只启动一个线程。此时会形成一个待执行线程的队列;

第 38 行,参数 [id] 为生成的线程命名。 此处所有 [nbAleas] 线程均使用相同名称 [alea]。这将使我们能够同时取消所有线程。若不处理线程取消,此参数为可选;

  • 第 44 行:调用活动中的 [getAlea] 方法。该方法将在与 UI 不同的线程中执行。UI 将调用 Web 服务,但不会等待响应。稍后,它将通过事件通知响应已可用。 此时,第44行将调用[showInfo]方法,并将其收到的响应作为参数传入;
  • 第45-47行:Web请求的执行可能会引发异常。此时,系统会要求将异常的错误消息显示在警报消息中;
  • 第35行:进入结果等待状态:
    • 将显示一个等待指示器;
    • 按钮 [Annuler] 将替换按钮 [Exécuter]。由于启动的线程是异步的,UI 线程不会等待它们,因此第 35 行会在它们结束前执行。 一旦 [beginWaiting] 方法结束,UI 即可再次响应用户的请求,例如点击 [Annuler] 按钮。 如果启动的线程是同步的,则只有在所有线程都结束之后,才会到达第 35 行。此时取消这些线程就不再有意义;

方法 [showInfo] 如下:


  @UiThread
  protected void showInfo(int alea) {
    if (!hasBeenCanceled) {
      // 更多信息
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // 是否已完成?
      if (nbInfos == nbAleas) {
        // 结束等待
        cancelWaiting();
      }
      // 将信息添加到响应列表中
      reponses.add(0, String.valueOf(alea));
      // 显示回答
      adapterReponses.notifyDataSetChanged();
    }
}
  • 方法 [showInfo] 在由 [@Background] 注解的线程 [getAlea] 内部被调用。该方法将更新视觉界面 UI。 它只能在 UI 线程内部执行时才能完成此操作。这就是第 1 行注解 [@UiThread] 的含义;
  • 第2行:该方法接收一个随机数;
  • 第3行:只有当用户未取消请求时,方法主体才会被执行;
  • 第5-6行:递增响应计数器并显示其值;
  • 第8-11行:若已收到所有预期响应,则结束等待(等待信号结束,按钮[Exécuter]替换按钮[Annuler]);
  • 第12-15行:将接收到的随机数添加到[ListView listReponses]组件显示的响应列表中,并刷新该组件;

方法 [showAlert] 如下:


  @UiThread
  protected void showAlert(Throwable th) {
    if (!hasBeenCanceled) {
      // 取消所有操作
      doAnnuler();
      // 显示
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}

其逻辑与方法 [showInfo] 类似:

  • 第 1 行:注解 [@UiThread] 是必需的;
  • 第 2 行:该方法接收发生的异常;
  • 第 3 行:仅当用户未取消请求时,该方法才会执行;
  • 第 5 行:取消用户的请求,如同用户亲自点击了 [Annuler] 按钮一样;
  • 第 7 行:使用 Android 类 [AlertDialog] 显示警报:
    • [activity]:是存储在父类 [AbstractFragment] 中的 [Activity] 类型活动;
    • [setTitle]:设置警报窗口 [1] 的标题;
    • [setMessage]:设置警报窗口 [2] 显示的消息;
    • [setNeutral]:设置用于关闭警报窗口的按钮 [3];
    • [show]:请求显示警报窗口;
 

对按钮 [Annuler] 的“点击”操作通过以下方法进行处理:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // 内存
    hasBeenCanceled=true;
    // 取消异步任务
    BackgroundExecutor.cancelAll("alea", true);
    // 等待结束
    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">
      <!-- 等待图像 -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>

    </android.support.v7.widget.Toolbar>
    <!-- 等待图像 -->
  </android.support.design.widget.AppBarLayout>
...
  • 第 17-21 行:加载图片;

1.16.2.7.2. 业务活动 [MainActivity]

[MainActivity] 活动与 [Exemple-14] 相比变化不大。首先,向其注入 [DAO] 层:


  // DAO注入
  @Bean(Dao.class)
  protected IDao dao;
...
  @AfterInject
  protected void afterInject() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // 配置图层 [DAO]
    setTimeout(TIMEOUT);
}
  • 第2-3行:通过注释AA注入[DAO]层;
  • 第5-13行:注入后执行的代码;
  • 第12行:将[DAO]层的timeout固定

此外,活动 [MainActivity] 必须实现接口 [IMainActivity],而该接口本身又继承自接口 [IDao]:


  // 实现 IMainActivity --------------------------------------------------------------------
  @Override
  public void navigateToView(int position) {
    // 显示位置视图
    if (mViewPager.getCurrentItem() != position) {
      // 显示片段
      mViewPager.setCurrentItem(position);
    }
  }

  // 待机图像管理
  public void cancelWaiting() {
    loadingPanel.setVisibility(View.INVISIBLE);
  }

  public void beginWaiting() {
    loadingPanel.setVisibility(View.VISIBLE);
  }

  // 实现IDao --------------------------------------------------------------------

  @Override
  public int getAlea(int a, int b) {
    // 执行
    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 客户端:

Image

要了解应将什么内容放入 [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 个随机数,且不设置超时;
  • 在 [10000, 20000] 区间内生成 2000 个随机数,不设置等待时间,并在生成结束前取消等待;
  • 在区间 [100, 200] 内生成 5 个随机数,设置 5000 毫秒的等待时间,并在生成结束前取消等待;

1.16.2.9. 取消处理

为了跟踪用户请求取消或因发生异常而请求取消时的情况,我们在接口 [IDao] 中添加了以下方法(参见第 1.16.2.4.1 节):


package exemples.android.dao;

public interface IDao {

  ...

  // 调试模式
  void setDebugMode(boolean isDebugEnabled);
}

在类 [Dao] 中,我们添加以下代码:


  // 调试模式
  private boolean isDebugEnabled;
  // 类名
  private String className;
..
  // 构造函数
  public Dao() {
    // 类名
    className = getClass().getSimpleName();
  }
...
  // 接口 IDao -------------------------------------------------------------------
  @Override
  public int getAlea(int a, int b) {
    // 日志
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // 服务执行
    Response<Integer> info;
...
  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
}
  • 第 9 行:记录类名;
  • 第 16-18 行:每次调用方法 [getAlea] 时,我们都会写入一条日志;

此外,在代码片段 [Vue1Fragment] 中,我们添加以下日志:


  @UiThread
  protected void showInfo(int alea) {
    // 日志
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    ....
  }

  @UiThread
  protected void showAlert(Throwable th) {
    // 日志
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    ...
    }
}

  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // 日志
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
   ...
}

每当片段 [Vue1Fragment] 从 [DAO] 层接收信息时,都会生成一条日志。此外,当调用方法 [doAnnuler] 时,会记录该事件。

测试 1

在服务器尚未启动的情况下请求5个数字。得到以下日志:

06-06 08:48:51.571 15317-16201/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.576 15317-16202/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.585 15317-16204/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.586 15317-16203/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.593 15317-16205/exemples.android D/Dao_: getAlea [100, 200] en cours
...
06-06 08:48:53.568 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.568 15317-15317/exemples.android D/Vue1Fragment_: Annulation demandée
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
  • 第1-5行:[Dao]类的[getAlea]方法被调用五次。需要说明的是,这些是由[VueFragment]片段发起的异步调用,且该片段不会等待调用结果;
  • 第7行:第一个请求HTTP已发出,片段[VueFragment]收到了其第一个异常;
  • 第 8 行:随后它请求取消所有请求;
  • 第9-12行:然而可以看到,它随后收到了以下四个异常。因此,所有处于等待状态的异步请求均已执行完毕;

测试 2

现在,启动服务器并请求5个数字,设置5秒延迟,并在该延迟结束前点击[Annuler]。日志如下:

06-06 09:12:38.360 4640-5054/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.360 4640-5055/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.361 4640-5056/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.362 4640-5057/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.363 4640-5058/exemples.android D/Dao_: getAlea [100, 200] en cours
...
06-06 09:12:39.895 4640-4640/exemples.android D/Vue1Fragment_: Annulation demandée
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:30:00.150 1616-1616/exemples.android D/Vue1Fragment_: showInfo(157)
06-06 09:30:00.151 1616-1616/exemples.android D/Vue1Fragment_: showInfo(157)
  • 第1-5行:[Dao]类的[getAlea]方法被调用五次;
  • 第 7 行:用户请求取消这些请求;
  • 第8行:可见[Vue1_Fragment]接收了5个值。再次,所有待处理的异步请求均已执行完毕;

这就是为什么我们需要处理一个布尔值 [hasBeenCanceled],以避免在已请求取消的情况下显示任何内容。在取消代码中:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // 日志
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // 内存
    hasBeenCanceled = true;
    // 取消异步任务
    BackgroundExecutor.cancelAll("alea",true);
    // 等待结束
    cancelWaiting();
}

第 10 行的代码未按预期运行。这可能是因为异步任务共享了同一注解方法 [@Background]:


  @Background(id = "alea")
  void getAlea(int a, int b) {
    ...
}

1.17. 示例-16:使用 RxAndroid 处理异步操作

现在,我们将使用名为 RxJava [http://reactivex.io/] 的库及其针对 Android 环境的衍生版本 [RxAndroid] 来处理 Android 应用所需的异步操作。 为此,我们将使用课程 [Introduction à RxJava. Application aux environnements Swing et Android]。

1.17.1. 创建项目

我们将项目 [Exemple-1] 复制为 [Exemple-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 {

  // 随机数
  Observable<Integer> getAlea(int a, int b);

  // Web 服务的 URL
  void setUrlServiceWebJson(String url);

  // 服务器响应的最大超时时间(毫秒)
  void setTimeout(int timeout);

  // 客户端在发送请求前的等待时间(毫秒)
  void setDelay(int delay);

  // 调试模式
  void setDebugMode(boolean isDebugEnabled);
}
  • 第8行:方法[getAlea]现在返回RxJava库(第3行)中的类型[Observable]。其原理如下:

一个类型为 Observable<T> 的元素流由一个或多个类型为 Subscriber<T> 的订阅者(订阅者、观察者、消费者)进行观察。 RxJava 库允许 Observable<T> 流在 T1 线程中运行,其 Subscriber<T> 观察者在 T2 线程中运行,而开发者无需无需担心管理这些线程的生命周期,也无需处理诸如线程间数据共享和为执行全局任务而进行的线程同步等自然而然的难题。因此,它简化了异步编程。

1.17.5. [AbstractDao] 类

我们将让 [Dao] 类从以下 [AbstractDao] 类派生:


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 {

  // 映射器 jSON
  private ObjectMapper mapper = new ObjectMapper();

  // 受保护的方法 ----------------------------------------------------------
  // 通用接口
  protected interface IRequest<T> {
    Response<T> getResponse();
  }

  // 通用请求
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // 服务执行
    return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // 服务执行
        try {
          // 执行同步请求并将响应转发给订阅者
          Response<T> response = request.getResponse();
          // 错误?
          int status = response.getStatus();
          if (status != 0) {
            // 记录异常
            ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
          } else {
            // 发送响应
            subscriber.onNext(response.getBody());
            // 报告可观察对象结束
            subscriber.onCompleted();
          }
        } catch (JsonProcessingException | RuntimeException e) {
          // 记录异常
          ex = new DaoException(e, 100);
        }
        // 异常?
        if (ex != null) {
          // 抛出异常
          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 行:从响应中获取 status
  • 第 32-34 行:如果该 status 属于错误,则准备一个异常;
  • 第36-39行:如果该status不是错误响应,则返回客户端实际期望的响应(第37行),并告知监听器不再有其他响应(第39行);
  • 第 41-44 行:如果请求 HTTP 以异常结束,则记录该异常;
  • 第46-49行:如果异常[ex]与null不同,则将其发送给观察者。 此时无需调用方法 [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 {

  // 服务客户端 REST
  @RestService
  protected WebClient webClient;

  // 执行请求前的等待延迟
  private int delay;
  // 调试模式
  private boolean isDebugEnabled;
  // 类名
  private String className;

  // 构造函数
  public Dao() {
    // 类名
    className = getClass().getSimpleName();
  }


  // 接口 IDao -------------------------------------------------------------------
  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // 日志
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // Web 客户端执行
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // 等待
        waitSomeTime(delay);
        // 同步调用 HTTP
        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>.getReponse()] 应返回的类型;
  • 第 36 行:此处使用了一个名为 closure 的属性:该属性能够在实例创建时将其外部值封装到实例中,此处即为第 24 行中 [a, b] 的值。 正是这一特性使得方法 [IRequest<Integer>.getReponse()] 无需参数。这些参数已被固化在方法主体中。 而在通常需要将方法参数 (a,b) 更改为 (x,y) 的情况下,这里创建了一个新的 [IRequest<Integer>] 实例,并封装了 x 和 y 的值;

1.17.7. [MainActivity] 类

实现接口 [IDao] 的类 [MainActivity] 演变如下:


  // IDao 实现 --------------------------------------------------------------------

  @Override
  public Observable<Integer> getAlea(int a, int b) {
    // 执行
    return dao.getAlea(a, b);
}

1.17.8. 类 [Vue1Fragment]

类 [Vue1Fragment] 的演变如下:


  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // 清除先前响应
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // 将响应计数器重置为0
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // 验证输入数据的有效性
    if (!isPageValid()) {
      return;
    }
    // 初始化活动
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // 请求随机数
    getAleasInBackground(a, b);
    // 开始等待
    beginWaiting();
}
  • 第 18 行:向 [getAleasInBackground] 方法请求随机数,该方法之所以这样命名,是因为随机数将在与 UI 不同的线程中请求;

  private int nbReponses = 0;
  // 订阅可观察对象
  private List<Subscription> abonnements;

// 注释 [Background] 无用
  void getAleasInBackground(int a, int b) {
    // 起初没有响应,也没有订阅
    nbReponses = 0;
    abonnements.clear();
    // 正在准备可观察对象
    Observable<Integer> response = Observable.empty();
    // 合并不同调用的结果 HTTP
    // 它们在 I/O 线程上执行
    for (int i = 0; i < nbAleas; i++) {
      response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
    }
    // 累积的可观察对象将在 UI 线程上被观察
    response = response.observeOn(AndroidSchedulers.mainThread());
    try {
      // 执行该可观察对象
      abonnements.add(response.subscribe(new Action1<Integer>() {
        @Override
        public void call(Integer alea) {
          // 将信息添加到响应列表中
          showInfo(alea);
        }
      }, new Action1<Throwable>() {
        @Override
        public void call(Throwable th) {
          // 错误消息
          showAlert(th);
          // 等待结束
          doAnnuler();
        }
      }, new Action0() {
        @Override
        public void call() {
          // 等待结束
          cancelWaiting();
        }
      }));
    } catch (RuntimeException e) {
      // 在UiThread中显示异常
      showAlert(e);
    }
}
  • 第 3 行:一个可观察对象拥有订阅者。订阅者与其所观察的进程之间的关联称为订阅(Subscription)。在此,我们仅有一个被观察的进程和一个订阅者,因此仅存在一个订阅。从原理上讲,我们假设可以有多个被观察的进程由不同的观察者观察,从而形成多个订阅;
  • 第11-18行:配置被观察的进程(可观察对象)。需要注意的是,这仅是配置操作:该进程并未被执行;
  • 第 11 行:从一个空的可观察对象开始,该对象不发出任何信号;
  • 第14-16行: 向该空可观测对象添加 [nbAleas] 可观测对象,这些将作为 [nbAleas] 请求,并返回 HTTP 随机数;
  • 第 15 行:与之前一样,向 [MainActivity] 类请求第 i 个随机数。 必须清楚,此时尚未执行任何 HTTP 请求。[mainActivity.getAlea(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] 的演变如下:


  // 注释 [UiThread] 无用
  protected void showInfo(int alea) {
    // 日志
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    if (!hasBeenCanceled) {
      // 更多信息
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // 将信息添加到回答列表中
      reponses.add(0, String.valueOf(alea));
      // 显示答案
      adapterReponses.notifyDataSetChanged();
    }
}

该方法有两处变更:

  • 第 1 行:删除了注释 AA [@UiThread];
  • 不再通过计数响应来判断是否应停止等待。现在由可观察对象的事件 [onCompleted] 提供此信息;

方法 [showAlert] 的变更如下:


  // 注释 [UiThread] 无用
  protected void showAlert(Throwable th) {
    // 日志
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    if (!hasBeenCanceled) {
      // 取消所有操作
      doAnnuler();
      // 出现在海报上
      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() {
    // 日志
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // 内存
    hasBeenCanceled = true;
    // 取消异步任务
    if (abonnements != null) {
      for (Subscription abonnement : abonnements) {
        abonnement.unsubscribe();
      }
    }
    // 等待结束
    cancelWaiting();
}
  • 第12行:取消订阅,从而停止对相关进程的监视;

1.17.9. 执行

启动 Web 服务(参见第 1.16.1.7 节),启动 Android 客户端,并重新执行您在上一示例中进行的测试(参见第 1.16.2.8 节)。

1.17.10. 取消管理

重复执行与前一个示例相同的测试(第 1.16.2.9 节)。

测试 1

在服务器尚未启动的情况下请求 5 个数字。得到以下日志:

1
2
3
4
5
6
7
06-07 05:48:09.790 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:11.789 28272-28272/exemples.android D/Vue1Fragment_: Exception reçue
06-07 05:48:11.789 28272-28272/exemples.android D/Vue1Fragment_: Annulation demandée

第7行之后,日志记录中断,这表明观察者(Vue1Fragment)不再收到被观察进程的通知。

测试 2

现在,启动服务器并以5秒的间隔请求5个数字,并在该间隔结束前点击[Annuler]。日志如下:

1
2
3
4
5
6
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:23.485 28272-28272/exemples.android D/Vue1Fragment_: Annulation demandée

第 6 行之后,日志记录停止,这表明监视器 (Vue1Fragment) 不再接收被监视进程的任何通知。

这正是取消操作的预期行为。因此,在 [Vue1Fragment] 的代码中,我们可以移除之前示例中引入的布尔变量 [hasBeenCanceled],因为取消操作并未达到预期效果。

观察者在可观察对象被取消后不再接收通知,并不意味着 HTTP 请求本身也被取消。为了验证这一点,我们将 [Dao] 类修改如下:


  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // 日志
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // Web 客户端执行
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // 等待
        waitSomeTime(delay);
        // 调用 HTTP(同步)
        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 的日志如下:

06-07 06:03:20.778 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.784 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:21.493 27085-27085/exemples.android D/Vue1Fragment_: Annulation demandée
06-07 06:03:21.636 27085-27440/exemples.android D/Dao_: response [{"body":176,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27442/exemples.android D/Dao_: response [{"body":145,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27439/exemples.android D/Dao_: response [{"body":197,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27438/exemples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27441/exemples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
  • 第1-5行:已发出5个请求;
  • 第 6 行:用户已取消;
  • 第7-11行:成功接收了五个HTTP请求的响应。但由于可观察对象被取消,这些元素并未传递给观察者;

1.17.11. 结论

在本文档的后续内容中,客户端/服务器应用程序将使用库 RxAndroid 而不是库 AA,原因如下:

  1. RxAndroid 可在不使用 AA 的 Android 应用程序中使用;
  2. RxAndroid 不仅能简化异步操作,还提供了大量方法,用于基于另一个可观察对象创建新的可观察对象。这些方法在 AA 中没有对应功能;
  3. 一旦想要派生一个带有 AA 注解的类(例如片段),就会遇到严重的问题。因此,我们不得不放弃 AA,转而使用方案 1 进行异步编程;

若读者希望深入了解 RxAndroid 库的功能,可参阅文档 [Introduction à RxJava. Application aux environnements Swing et Android]。该文档中在不使用 AA 库的情况下,采用了 RxAndroid。

1.18. 示例-17:数据输入组件

我们将编写一个新项目,以展示数据输入表单中的一些常用组件。

1.18.1. 创建项目

我们将项目 [Exemple-13] 复制为 [Exemple-17]:

新项目将仅包含一个视图 [vue1.xml]。因此,我们将删除视图 [vue2.xml] 及其关联的片段 [Vue2Fragment] 和 [2]。 我们在 [Mainactivity] 的片段管理器中考虑了此更改:


  // 我们的片段管理器需针对每个应用程序重新定义
  // 必须定义以下方法:getItem、getCount、getPageTitle
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // 片段
    private final Fragment[] fragments = {new Vue1Fragment_()};
....
}

重新运行该项目。它应像之前一样显示视图 1。我们将以此项目为基础进行后续操作。

1.18.2. 表单视图 XML

  

文件 [vue1.xml] 生成的视图如下:

Image

视图中的文本 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>

表单的主要组件如下:

  • 第 2 行:一个垂直布局 [ScrollView]。它允许
  • 呈现一个比平板电脑屏幕更大的表单
  • 。通过滚动可查看表单的全部内容
  • 滚动查看;
 
  • 第125-132行:一个复选框
  • 第134-159行:一组三个单选按钮
  • 第 161-166 行:一个搜索栏
  • 第 16-176 行:一个输入框
  • 第 178-186 行:一个是/否开关
  • 第 188-195 行:一个时间输入框
  • 第197-207行:一个多行输入框
  • 第 209-215 行:一个下拉列表
  • 第 217-225 行:一个日期输入框
  • 所有其他组件均为 [TextView],用于显示文本。
 

1.18.3. 表单中的字符串

表单中的字符串在以下文件中定义:

  

<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>
  <!-- 视图 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. 表单片段

  

类 [Vue1Fragment] 如下所示:


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;

// 片段是由片段容器显示的视图
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {

  // 片段显示的视图中的字段
  @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;

  // 下拉列表
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;

  @AfterViews
  void afterViews() {
    // 勾选第一个按钮
    radioButton1.setChecked(true);
    // 日历
    datePicker1.setCalendarViewShown(false);
    // 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));
      }
    });
    // 下拉列表
    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() {
    // 下拉列表适配器的初始化
    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 [vue1](第 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<String> messages = new ArrayList<>();
    // 复选框
    boolean isChecked = checkBox1.isChecked();
    messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
    // 单选按钮
    int id = radioGroup.getCheckedRadioButtonId();
    String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
    messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
    // SeekBar
    int progress = seekBar.getProgress();
    messages.add(String.format("SeekBar [value=%d]", progress));
    // 输入框
    String texte = String.valueOf(saisie.getText());
    messages.add(String.format("Saisie simple [value=%s]", texte));
    // 切换按钮
    boolean état = switch1.isChecked();
    messages.add(String.format("Switch [value=%s]", état));
    // 日期
    int an = datePicker1.getYear();
    int mois = datePicker1.getMonth() + 1;
    int jour = datePicker1.getDayOfMonth();
    messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
    // 多行文本
    String lignes = String.valueOf(multiLignes.getText());
    messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
    // 时间
    int heure = timePicker1.getHour();
    int minutes = timePicker1.getMinute();
    messages.add(String.format("Heure [%d, %d]", heure, minutes));
    // 下拉列表
    int position = dropDownList.getSelectedItemPosition();
    String selectedItem = String.valueOf(dropDownList.getSelectedItem());
    messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
    // 显示
    doAfficher(messages);
}
  • 第 4 行:输入的值将被累加到消息列表中;
  • 第6行:方法 [CheckBox].isCkecked() 用于判断复选框是否被选中;
  • 第 9 行:方法 [RadioGroup].getCheckedButtonId() 可获取被选中的单选按钮的 ID,若未选中则返回 -1;
  • 第10行:代码[activity.findViewById(id)]可用于查找已选中的单选按钮,从而获取其标签;
  • 第 13 行:方法 [SeekBar].getProgress() 可获取滑块的数值;
  • 第 19 行:方法 [Switch].isChecked() 可判断一个开关是 On(true)还是 Off(false);
  • 第 22 行:方法 [DatePicker].getYear() 可通过 [DatePicker] 对象获取所选年份;
  • 第 23 行:方法 [DatePicker].getMonth() 可通过 [DatePicker] 对象获取所选月份,该对象位于 [0,11] 区间内;
  • 第 24 行:方法 [DatePicker].getDayOfMonh() 可根据 [1,31] 区间内的 [DatePicker] 对象获取所选月份的日期;
  • 第 30 行:方法 [TimePicker].getHour() 可通过 [TimePicker] 对象获取所选小时;
  • 第 31 行:方法 [TimePicker].getMinute() 可通过对象 [TimePicker] 获取选定的分钟;
  • 第 34 行:方法 [Spinner].getSelectedItemPosition() 可获取下拉列表中选定项的位置;
  • 第 35 行:方法 [Spinner].getSelectedItem() 可获取下拉列表中选中的对象;

显示已输入值列表的 [doAfficher] 方法如下:


    private void doAfficher(List<String> messages) {
        // 构建要显示的文本
        StringBuilder texte = new StringBuilder();
        for (String message : messages) {
            texte.append(String.format("%s\n", message));
        }
        // 显示文本
        new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
  • 第 1 行:该方法接收待显示的消息列表;
  • 第 3-6 行:根据这些消息构建一个 [StringBuilder] 对象。在字符串拼接方面,[StringBuilder] 类型比 [String] 类型更高效;
  • 第 8 行:一个对话框显示第 3 行中的文本:

Image

1.18.5. 项目运行

运行项目并测试各个输入组件。

1.19. 示例-18:使用视图模板

1.19.1. 创建项目

我们通过复制项目 [Exemple-13] 来创建一个新项目 [Exemple-18]。

1.19.2. 视图模板

我们希望将该项目的两个视图提取出来并纳入一个模板中:

  

Image

这两个视图的结构将完全相同:

  • 在 [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 行生成;

视图 XML [main] 使用来自文件 [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]:

Image

列表中的每个视图包含三个组件:

  • 一个用于显示信息的 [TextView];
  • 一个 [CheckBox];
  • 一个可点击的 [TextView];

1.20.1. 创建项目

我们通过复制项目 [Exemple-18] 来创建一个新的项目 [Exemple-19]。

  

我们将按照[3]中的说明推进该项目。

1.20.2. 该会话

  

会话存储了活动与片段之间共享的数据:


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 {
  // 数据列表
  private List<Data> liste=new ArrayList<>();

  // 获取器和设置器
...
}
  • 第 11 行:两个视图共用的数据列表;

类 [Data] 如下所示:


package exemples.android.architecture;

public class Data {

    // 数据
    private String texte;
    private boolean isChecked;

    // 构造函数
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }

    // 获取器和设置器
    ...
}
  • 第 6 行:用于填充列表中每个元素的第一个 [TextView] 的文本;
  • 第7行:用于决定是否勾选列表中每个元素的[checkBox]的布尔值;

1.20.3. [MainActivity] 活动

[@AfterInject]方法的代码如下:


  // 会话注入
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // 创建数据列表
    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. 初始视图 [Vue1]

视图 XML [vue1.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] 管理视图 XML [vue1]。其代码如下:


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 {

  // 片段显示视图中的字段
  @ViewById(R.id.listView1)
  protected ListView listView;
  // 列表适配器
  private ListAdapter adapter;
  // 初始化完成
  private boolean initDone = false;

  @AfterViews
  void afterViews() {
    // 内存
    afterViewsDone = true;
  }

  @Click(R.id.button_vue2)
  void navigateToView2() {
    // 导航至视图 2
    mainActivity.navigateToView(1);
  }

  public void doRetirer(int position) {
   ...
  }

  @Override
  protected void updateFragment() {
    if (!initDone) {
      // 将数据关联到 [ListView]
      adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
      initDone = true;
    }
    // 当片段被(重新)生成时——此时需将 ListView 重新关联至其适配器
    listView.setAdapter(adapter);
    // 其他片段更改了数据源的情况——此时需刷新 ListView
    adapter.notifyDataSetChanged();
  }
}
  • 第 15 行:视图 XML [vue1] 与该片段相关联;
  • 第26-30行:方法[@AfterViews]不执行任何操作。 但该方法对于将变量 [afterViewsDone] 设置为 true 是必要的,因为父类 [AbstractFragment] 会使用该变量;
  • 第 42-53 行:[updateFragment] 方法,该方法在片段即将显示时每次都会被调用。 此处编写该方法时,假设片段可能脱离显示片段的邻接关系,从而重置其生命周期。虽然此处并非如此,但如果应用程序拥有 3 个邻接度为 1 的片段,则会发生这种情况;
  • 第 44 行:[ListView] 的适配器只需初始化一次;
  • 第 46 行:将类型为 [ListAdapter] 的适配器与该 [ListView] 关联。 我们将构建该类。它继承自 [ArrayAdapter] 类,我们之前曾使用该类将数据关联到 [ListView] 上。我们将各种信息传递给 [ListAdapter] 的构造函数:
    • 当前活动的引用、
    • 将为列表中每个元素实例化的视图标识符,
    • 用于填充列表的数据源,
    • 一个片段引用。该引用将用于通过第 38 行中的 [doRetirer] 方法处理对 [ListView] 中 [Retirer] 链接的点击;
  • 第 50 行:适配器与 [ListView] 相关联。同时,数据源 [listes] 与 [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> {

    // 执行上下文
    private Context context;
    // 列表中某一行显示布局的ID
    private int layoutResourceId;
    // 列表数据
    private List<Data> data;
    // 显示 [ListView] 的片段
    private Vue1Fragment fragment;
    // 适配器
    final ListAdapter adapter = this;

    // 构造函数
    public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
        super(context, layoutResourceId, data);
        // 存储信息
        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行:[getView]方法将被[ListView]反复调用,以生成编号为[position]的元素视图。 生成的结果 [View] 是一个指向所创建视图的引用。

方法 [getView] 的代码如下:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // 创建当前行 ListView
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // 文本
        TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
        textView.setText(data.get(position).getTexte());
        // 复选框
        CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
        checkBox.setChecked(data.get(position).isChecked());
        // 链接[Retirer]
        TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
        txtRetirer.setOnClickListener(new OnClickListener() {

            public void onClick(View v) {
                fragment.doRetirer(position);
            }
        });
        // 处理复选框的点击
        checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {

            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                data.get(position).setChecked(isChecked);
            }
        });
        // 显示该行
        return row;
}
  • 第 2 行:该方法接收三个参数。我们仅使用第一个参数;
  • 第4行:创建编号为[position]的视图。该视图即[list_data],其ID作为第二个参数传递给了构造函数。随后获取刚刚实例化视图中各组件的引用;
  • 第 6 行:获取编号为 1 的 [TextView] 的引用;
  • 第7行:为其赋值来自数据源的文本,该数据源作为第三个参数传递给了构造函数;
  • 第9行:获取编号为2的[CheckBox]的引用;
  • 第10行:根据来自[ListView]数据源的值,决定是否勾选该项;
  • 第 12 行:获取 [TextView] 第 3 号的引用;
  • 第13-18行:处理对链接[Retirer]的点击;
  • 第16行:由方法[Vue1Fragment].doRetirer处理此点击。实际上,由显示[ListView]的片段来处理此事件似乎更为合理。 该片段拥有[ListAdapter]类所不具备的整体视图。片段[Vue1Fragment]的引用已被作为第四个参数传递给了该类的构造函数;
  • 第20-25行:处理复选框的点击事件。对复选框的操作会反映在其所显示的数据上。原因如下:[ListView]是一个列表,仅显示其中部分元素。因此,列表中的某个元素有时会被隐藏,有时会被显示。 当需要显示第 i 个元素时,会调用上文第 2 行中的 [getView] 方法来处理第 i 个位置。第 10 行将根据复选框关联的数据重新计算其状态。 因此,该数据必须随时间推移记录复选框的状态;

1.20.8. 从列表中移除一个元素

点击链接 [Retirer] 的操作由片段 [Vue1Fragment] 中的以下方法 [doRetirer] 处理:


  public void doRetirer(int position) {
    // 从列表中移除编号为 [position] 的元素
    List<Data> liste = mainActivity.getListe();
    liste.remove(position);
    // 记录滚动位置以便返回
    // 读取
    // [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // 第一个元素是否完全可见的位置
    int firstPosition = listView.getFirstVisiblePosition();
    // 该元素相对于顶部的 Y 偏移量ListView
    // 测量可能被遮挡部分的高度
    View v = listView.getChildAt(0);
    int top = (v == null) ? 0 : v.getTop();
    // 刷新 [ListView]
    adapter.notifyDataSetChanged();
    // 定位到 ListView 的正确位置
    listView.setSelectionFromTop(firstPosition, top);
}
  • 第 1 行:获取被点击的链接 [Retirer] 在 [ListView] 中的位置;
  • 第3行:获取数据列表;
  • 第 4 行:移除编号为 [position] 的元素;
  • 第15行:刷新[ListView]。如果不这样做,视觉上不会有任何变化。
  • 第5-13行、第17行:操作较为复杂。若不执行此步骤,将出现以下情况:
    • [ListView] 会显示数据列表的第 15-18 行,
    • 删除第16行,
    • 上方的第15行将其完全重置,随后[ListView]便显示数据列表的第0-3行;

通过上述操作,删除完成后,[ListView] 仍停留在被删除行之后的行上。

1.20.9. 视图 XML [Vue2]

该视图的代码 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 行:第 1 个组件 [TextView];
  • 第 26-33 行:第 2 个组件 [TextView];
  • 第 17-24 行:第 3 个组件 [Button];

1.20.10. 片段 [Vue2Fragment]

123

片段 [Vue2Fragment] 管理视图 XML [vue2]。其代码如下:


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 {

    // 视图中的字段
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;

    @AfterViews
    void initFragment(){
        // 内存
        afterViewsDone=true;
    }

  @Click(R.id.button_vue1)
    void navigateToView1() {
        // 导航至视图 1
        mainActivity.navigateToView(0);
    }

    @Override
    protected void updateFragment() {
        // 显示在视图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]显示的数据列表。该列表存储在业务活动中;
  • 第36行:如果第i项数据已被勾选,则将关联的标签添加到[StringBuilder]类型中;
  • 第 41 行:[TextView] 显示计算出的文本;

1.20.11. 执行

为该项目创建一个运行配置并执行。

1.20.12. 优化

在上例中,我们使用了数据源 List<Data>,其中类 [Data] 如下所示:


package exemples.android.fragments;

public class Data {

    // 数据
    private String texte;
    private boolean isChecked;

    // 制造商
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }
...

}

在第 7 行,我们使用了一个布尔值来管理 [ListView] 元素的复选框。 通常,[ListView] 需要显示可通过勾选复选框来选择的数据,但数据源中的元素未必拥有与该复选框对应的布尔字段。此时可按以下方式处理:

[Data] 类将变为如下形式:


package exemples.android.fragments;

public class Data {

    // 数据
    private String texte;

    // 构造函数
    public Data(String texte) {
        this.texte = texte;
    }

    // 获取器和设置器
...
}

创建一个从上述类派生的类 [CheckedData]:


package exemples.android.fragments;

public class CheckedData extends Data {

    // 复选框
    private boolean isChecked;

    // 构造函数
    public CheckedData(String text, boolean isChecked) {
        // 父类
        super(text);
        // 局部
        this.isChecked = isChecked;
    }

    // getter 和 setter
...
}

随后只需在代码中所有位置(MainActivity、ListAdapter、Vue1Fragment、Vue2Fragment) 将类型 [Data] 替换为类型 [CheckedData]。例如在 [MainActivity] 中:


  @AfterInject
  protected void afterInject() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // 创建数据列表
    List<CheckedData> liste = session.getListe();
    for (int i = 0; i < 20; i++) {
      liste.add(new CheckedData("Texte n° " + i, false));
    }
}

此版本的项目文件以 [Exemple-19B] 的名称提供给您。

1.21. 示例-20:使用菜单

1.21.1. 创建项目

我们将项目 [Exemple-19B] 复制到项目 [Exemple-20] 中:

3

我们将删除视图 1 和 2 中的按钮,并用菜单选项 [1-2] 替换它们。

1.21.2. 菜单的定义

  

文件 [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 {

  // 数据  可供子类访问
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  protected String className;

  // 活动
  protected IMainActivity mainActivity;
  protected Activity activity;

  // 会话
  protected Session session;

  // 菜单
  private Menu menu;
  private int[] menuOptions;
  private boolean initDone;

  // 构造函数
  public AbstractFragment() {
    // 初始化
    className = getClass().getSimpleName();
    // 日志
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }

@Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // 内存
    this.menu = menu;
    // 日志
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    // 如果尚未执行,则获取菜单中的 # 选项
    if (!initDone) {
      // 获取菜单中的 # 个选项
      List<Integer> menuOptionsIds = new ArrayList<>();
      getMenuOptions(menu, menuOptionsIds);
      // 将选项列表转换为数组
      menuOptions = new int[menuOptionsIds.size()];
      for (int i = 0; i < menuOptions.length; i++) {
        menuOptions[i] = menuOptionsIds.get(i);
      }
      // 操作
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = this.mainActivity.getSession();
      // 内存
      initDone = true;
    }

    // 要求子片段进入
    updateFragment();
  }


  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
   ...
  }

  // 显示菜单选项 -----------------------------------
  protected void setAllMenuOptions(boolean isVisible) {
    ....
  }

  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    ...
  }

  // 更新子类
  protected abstract void updateFragment();
}
  • 第 42 行:日志显示,每次显示片段时都会调用方法 [onCreateOptionsMenu]。该方法被调用的时间非常晚,特别是当方法 [updateFragment] 已被调用之后。 这表明该方法可能用于更新片段。我们将在这里(第 63 行)进行此操作;
  • 第 42 行:该方法有两个参数:
    • [menu]:即一个空菜单;
    • [inflater]:一个根据初始描述创建菜单的工具。此处我们不会使用该功能,因为我们将使用注解 AA 来代为完成;
  • 第 44 行:我们将菜单保存起来。稍后我们会用到它;
  • 第52-53行:我们将菜单中所有元素的标识符存储在第28行的数组中;
  • 第 55-57 行:日志显示,当调用 [onCreateOptionsMenu] 方法时,[Fragment.getActivity()] 方法会将活动与片段关联起来;
  • 第 55 行:我们将该活动作为 Android 类 [Activity] 的实例进行存储;
  • 第 56 行:我们将该 Activity 作为 [IMainActivity] 接口的实例进行存储;
  • 第 57 行:我们将会话保存下来;
  • 第 59 行:我们注意到该类的初始化已完成,因此无需重复初始化(第 50 行);
  • 第63行:要求子片段进行更新。这是可行的,因为该片段既可见,又通过 关联了其视图和菜单;

用于获取菜单元素标识符的方法 [getMenuOptions] 如下:


  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // 遍历菜单中的所有项目
    for (int i = 0; i < menu.size(); i++) {
      // 第 i 个项目
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // 如果第 i 个项目是子菜单,则重新开始
      if (menuItem.hasSubMenu()) {
        // 递归
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
}

方法 [setAllMenuOptions] 可用于隐藏/显示菜单中的所有选项;


  protected void setAllMenuOptions(boolean isVisible) {
    // 更新菜单中的所有选项
    for (int menuItemId : menuOptions) {
      menu.findItem(menuItemId).setVisible(isVisible);
    }
}

方法 [setMenuOptions] 用于隐藏/显示菜单中的部分选项;


  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    // 更新菜单中的某些选项
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}

类 [MenuItemState] 如下所示:

  

package exemples.android.architecture;

public class MenuItemState {

  // 菜单选项的标识符
  private int menuItemId;
  // 选项的可见性
  private boolean isVisible;

  // 构造函数
  public MenuItemState() {

  }

  public MenuItemState(int menuItemId, boolean isVisible) {
    this.menuItemId = menuItemId;
    this.isVisible = isVisible;
  }

  // 获取器和设置器
...
}

1.21.4. [Vue1Fragment]片段中的菜单管理

类 [Vue1Fragment] 变为如下:


@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {

...

  @OptionsItem(R.id.navigationVue2)
  void navigateToView2() {
    // 跳转至视图 2
    mainActivity.navigateToView(1);
  }

  @OptionsItem(R.id.actionValider)
  void valider() {
    // 显示一条消息
    Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
  }

  private boolean actionCacherMontrerTout = true;
  @OptionsItem(R.id.actionCacherMontrerTout)
  void cacherMontrerTout() {
    // 状态变更
    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() {
    // 状态变更
    actionCacherMontrerActions = !actionCacherMontrerActions;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
  }

  private boolean actionCacherMontrerActionsValider = true;
  @OptionsItem(R.id.actionCacherMontrerActionsValider)
  void actionCacherMontrerActionsValider() {
    // 状态变更
    actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
  }
...

  @Override
  protected void updateFragment() {
    ....
    // 更新菜单
    //setMenuOptions(...)
  }
}
  • 第 2 行:菜单 [res / menu / menu_vue1.xml] 与片段相关联;
  • 第 48 行:当方法 [updateFragment] 执行时,菜单也可随之更新以反映片段的新状态;
  • 第 7 行:注解 [@OptionsItem(R.id.navigationVue2)] 标注了在点击菜单选项 [Navigation / Vue 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 {

  // 视图字段
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;

  @OptionsItem(R.id.navigationVue1)
  void navigateToView1() {
    // 导航至视图 1
    mainActivity.navigateToView(0);
  }

  @Override
  protected void updateFragment() {
    // 显示在视图 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);
    // 更新菜单
    // setMenuOptions(...)
  }
}
  • 第 35 行:显示选项 [Navigation / Vue 1];
  • 第17-20行:点击选项[Navigation / Vue1]时,调用方法[navigateToView1];

1.21.6. 执行

为该项目创建一个运行环境并执行它。

1.22. 示例-21:重构抽象类 [AbstractFragment]

前面的示例向我们展示了,当片段带有菜单时,其方法 [onCreateOptionsMenu] 是请求片段更新的理想位置:

  • 该方法在片段即将显示时会被调用一次;
  • 调用时,片段与其活动、视图及菜单的关联已建立;

为了说明这一点,我们重新使用示例 12,该示例的特点是包含许多可以修改其邻接关系的片段。在此示例中,片段原本没有菜单。我们将为它们关联一个空菜单。

1.22.1. 创建项目

我们将项目 [Exemple-12] 复制到项目 [Exemple-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>

这里需要理解的是,该活动已有自己的菜单 [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>

当一个活动已有菜单时,片段关联的菜单会添加到该活动的菜单中:因此我们拥有两个菜单的选项。在此情况下,片段菜单将为空。因此我们只会看到该活动的菜单。

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:保存/恢复活动和片段的状态

1.23.1. 问题

本文将探讨 Android 设备旋转(纵向 <--> 横向)的问题。为说明此问题,我们将沿用前面的示例 21:

Image

如果旋转设备 [1],我们将获得以下新视图:

Image

我们可以看到:

  • 在 [1] 状态下,标签页 [Fragment n° 3] 已消失;
  • 在 [2] 中,显示的文本确实是片段 3 的内容,但访问计数器不正确;

在此轮更新中,日志如下:

07-13 04:08:27.188 1677-1677/exemples.android D/MainActivity: constructor
07-13 04:08:27.189 1677-1677/exemples.android D/MainActivity: afterInject
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.194 1677-1677/exemples.android D/MainActivity: afterViews
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.203 1677-1677/exemples.android D/PlaceholderFragment: afterViews 4 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.204 1677-1677/exemples.android D/PlaceholderFragment: afterViews 3 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.208 1677-1677/exemples.android D/Vue1Fragment: afterViews Vue1Fragment_ - numVisit=0
07-13 04:08:27.208 1677-1677/exemples.android D/PlaceholderFragment: afterViews 2 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.209 1677-1677/exemples.android D/PlaceholderFragment: afterViews 1 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.351 1677-1677/exemples.android D/menu: création menu en cours
07-13 04:08:27.351 1677-1677/exemples.android D/PlaceholderFragment_: création menu en cours
07-13 04:08:27.351 1677-1677/exemples.android D/PlaceholderFragment: update 3 - PlaceholderFragment_ - numVisit=0, initDone=true, getActivity()==null:false
  • 第1行:可见该任务已完全重建;
  • 第3-7行:该活动管理的五个片段均已完全重建;
  • 第21行:将显示第3号片段。可见在递增之前,访问编号为0;

因此,旋转后的结果可解释如下:

  • 类 [MainActivity] 最初创建了一个仅含一个标签页的标签栏,该标签页名为 [Vue 1]。这就是当前可见的标签页;
  • 设备旋转后,页面管理器 [mViewPager] 重新显示了同一片段,即此处的第 3 号片段。需注意的是,标签页和片段是不同的概念,且具有不同的生命周期。第 3 号片段的 [updateFragment] 方法将执行:

  public void updateFragment() {
    // 日志
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // 访问次数递增
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // 修改文本
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
  • 第 7 行:从会话中读取了最后一次访问的编号。然而,该会话与其他所有内容一样已被重建,访问编号已被重置为零。这解释了片段 3 中显示的结果;

1.23.2. 活动和片段的备份/恢复方法

1.23.2.1. 解决方案 1:手动备份

设备旋转时,会调用该活动的两个方法:


// 活动备份/恢复管理 ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // 父级
    super.onSaveInstanceState(outState);
    // 活动状态备份
    // ....
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // 父级
    super.onCreate(savedInstanceState);
     // 活动恢复
    // ...
  }
  • 第2-8行:旋转时系统会调用方法[onSaveInstanceState]。此处可进行活动状态的备份。若不采取任何操作,则不会进行备份。 活动状态的保存必须在传递给该方法的参数 [Bundle outState] 中进行。 类 [Bundle] 类似于一个字典。它包含两个参数的方法 [putString, putInt, putLong, putBoolean, putChar, ...]:void putT(String key, T value)
  • 第10-16行:在创建活动时会调用方法 [onCreate]。如果该活动的状态已被保存,则该备份数据将通过参数 [Bundle savedInstanceState] 传递给该方法。 要检索已保存的值,可以使用诸如 [getString, getInt, getLong, geBoolean, getChar, ...] 这样的单参数方法:T getT(String key)

片段也具备这两个相同的方法来保存其状态。

我们将利用这些信息来保存和恢复示例 21 的状态。为此,我们将项目 [Exemple-21] 复制为 [Exemple-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. [Exemple-22] 项目的备份/恢复方法

项目 [Exemple-22] 的演变如下:

  

其中出现了两个新类:

  • [PlaceHolderFragmentState],用于存储 [PlaceHolderFragment] 类型片段的状态;
  • [Vue1FragmentState],用于存储类型为 [Vue1Fragment] 的片段的状态;

这些类如下:


package exemples.android;

public class Vue1FragmentState {
  // 状态 Vue1Fragment
  private boolean hasBeenVisited=false;
  // 获取器和设置器
...
}
  • 第 5 行:如果片段 [Vue1Fragment] 已被访问(显示)至少一次,则布尔值 [hasBeenVisited] 为真。该字段是为示例而创建的,因为片段 [Vue1Fragment] 没有需要保存的内容;

类 [PlaceHolderFragmentState] 如下:


package exemples.android;

public class PlaceHolderFragmentState {
  // 是否已访问状态
  private boolean hasBeenVisited;
  // 显示文本
  private String text;

  // 获取器和设置器
...
}
  • 第 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 {
  // 已访问片段数
  private int numVisit;
  // 在第二个标签页中显示的 [PlaceholderFragment] 类型片段编号
  private int numFragment = -1;
  // 所选标签页编号
  private int selectedTab = 0;
  // 当前视图编号
  private int currentView;

  // 片段备份 ---------------
  private Vue1FragmentState vue1FragmentState;
  private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];

  // 构造函数
  public Session() {
    for (int i = 0; i < placeHolderFragmentStates.length; i++) {
      placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
    }
    vue1FragmentState = new Vue1FragmentState();
  }
  // 获取器和设置器
...
}
  • 第18行:片段[Vue1Fragment]的状态;
  • 第19行:类型为[PlaceHolderFragment]的片段状态;
  • 第22-27行:在会话构造函数中,初始化第18行和第19行的字段;
  • 第12-15行:出现两个新字段:
    • 第 13 行:最后选中标签页的编号;
    • 第 15 行:最后显示的片段编号;

该活动以如下方式保存/恢复会话:


  // 活动备份/恢复管理 ----------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // 父级
    super.onSaveInstanceState(outState);
    // 会话保存
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // 日志
    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) {
    // 父级
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
      // 会话恢复
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // 日志
      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] 进行了如下演变:


// 备份/恢复管理 -----------------------------------------------
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // 父级
    super.setUserVisibleHint(isVisibleToUser);
    // 备份?
    if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
      // 该片段将被隐藏 - 正在备份
      saveFragment();
      saveFragmentDone = true;
    }
    // 内存
    this.isVisibleToUser = isVisibleToUser;
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // 父节点
    super.onActivityCreated(savedInstanceState);
    // 日志
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // 该片段需恢复
    fragmentHasToBeInitialized = true;
  }


  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // 日志
    if (isDebugEnabled) {
      Log.d(className, "onSaveInstanceState");
    }
    // 父级
    super.onSaveInstanceState(outState);
    // 仅当片段可见时才保存
    if (isVisibleToUser && !saveFragmentDone) {
      saveFragment();
      saveFragmentDone = true;
    }
  }

  // 子类
  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] 方法的变更如下:


// 更新片段
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // 内存
    this.menu = menu;
    // 日志
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    ...
    // 要求子片段进行更新
    updateFragment();
    // 需进行备份
    saveFragmentDone = false;
  }
  • 第 14 行:我们看到,当执行保存操作时,布尔值 [saveFragmentDone] 已更改为 vrai。在某个时刻,它必须恢复为 faux。 当子片段的 [updateFragment] 方法(第 12 行)执行时,该片段将变为可见。而片段必须在可见时进行保存,即在它从可见状态切换到隐藏状态的特定时刻。 因此,我们将布尔值 [saveFragmentDone] 设置为 false,以便进行快照保存;

1.23.4. 保存片段 [Vue1Fragment]

片段的保存操作在由父类 [AbstractFragment] 调用的方法 [saveFragment] 中进行:


// 片段状态备份
  @Override
  public void saveFragment() {
    // 日志
    if (isDebugEnabled) {
      Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
    }
    // 将片段状态保存至会话
    Vue1FragmentState state = new Vue1FragmentState();
    state.setHasBeenVisited(true);
    session.setVue1FragmentState(state);
    // 日志
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • 第 9-11 行:将片段状态保存到会话中。当调用方法 [saveFragment] 时,该片段处于可见状态。因此需将布尔值 [hasBeenVisited] 设置为 vrai(第 10 行);

1.23.5. 片段保存 [PlaceHolderFragment]

片段的保存操作在由父类 [AbstractFragment] 调用的方法 [saveFragment] 中进行:


  @Override
  public void saveFragment() {
    // 正在将片段状态保存到会话中
    PlaceHolderFragmentState state = new PlaceHolderFragmentState();
    state.setText(textViewInfo.getText().toString());
    state.setHasBeenVisited(true);
    session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
    // 日志
    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]被赋值为vrai
  • 第7行:片段的状态被存入[placeHolderFragmentStates]数组中。待初始化的元素编号为片段的章节编号减一;

1.23.6. 恢复片段 [Vue1Fragment]

片段的恢复在方法 [updateFragment] 中进行:


@Override
  protected void updateFragment() {
    // 日志
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
    }
    // 恢复?
    if (fragmentHasToBeInitialized) {
      // 状态恢复
      hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
      fragmentHasToBeInitialized = false;
    }
    // 日志
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
    }
    // 导航?
    boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
    if (navigation) {
      // 访问编号递增
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // 显示访问次数
      Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
    }
    // 更改当前视图编号
    session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
  }
  • 第8-12行:恢复片段状态。布尔值[fragmentHasToBeInitialized]由父类[AbstractFragment]初始化。 当其值为 vrai 时,表示片段刚被重建,需要对其进行重置。重置操作就在此处进行。在这个具体示例中,无需执行任何操作。 我们只是展示了如何在片段的保存状态中(第 10 行)获取布尔值 [hasBeenVisited];
  • 第11行:切记将[fragmentHasToBeInitialized]重置为faux,这样当后续返回该片段且设备未发生旋转时,便不会对片段进行不必要的初始化;
  • 第18-26行:访问计数器的递增。这里存在一个难题:在恢复片段时,我们不希望递增该计数器。我们需要在此区分以下两种情况:
    • 单纯的导航操作将用户带回 [Vue 1] 标签页;
    • 当用户在显示 [Vue 1] 标签页时旋转设备所导致的恢复;

通过会话中存储的视图编号,我们可以区分这两种情况。该编号即为最后显示的视图编号(第28行)。

  • 第18行:如果最后一个视图的编号与当前视图的编号不同,则执行导航操作而非恢复;
  • 第21-25行:访问计数器的递增及其显示;

1.23.7. 恢复片段 [PlaceHolderFragment]

片段的恢复在方法 [updateFragment] 中进行:


  // 数据
  private String text;
  private int numVisit;
  private String newText;
  private boolean hasBeenVisited = false;
  private ObjectMapper jsonMapper = new ObjectMapper();
...

public void updateFragment() {
    // 日志
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // 这是哪个片段?
    int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
    int numView = numSection - 1;
    // 该片段是否需要初始化?
    if (fragmentHasToBeInitialized) {
      // 初始文本
      text = getString(R.string.section_format, numSection);
      fragmentHasToBeInitialized = false;
    }
    // 导航?
    boolean navigation = session.getCurrentView() != numView;
    if (navigation) {
      // 访问次数递增
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // 修改后的文本
      newText = String.format("%s, visite %s", text, numVisit);
    } else {
      // 涉及恢复操作
      PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
      newText = state.getText();
    }
    // 显示文本
    textViewInfo.setText(newText);
    // 当前视图
    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中发现,在设备旋转时存在一个问题:只有第一个标签页[Vue 1]被保留,而第二个标签页则丢失了。

我们在类 [MainActivity] 中通过以下方式解决了此问题:


@AfterViews
  protected void afterViews() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "afterViews");
    }
    // 工具栏
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

 ...

    // 第一个标签页
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(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);
    }

    // 选择哪个标签页?
    tabLayout.getTabAt(session.getSelectedTab()).select();

...

  }
  • 第14-16行:创建第一个标签页;
  • 第18-23行:创建第二个标签页。为判断是否需要创建,需在会话中查询标签页2中显示的片段编号。若该编号不同于其初始值-1,则创建第二个标签页。此时将生成两个标签页,默认选中第一个;
  • 第26行:从会话中获取保存/恢复前被选中的标签页编号,并重新选中该标签页。如果字段[selectedTab]尚未被代码初始化,则使用其初始值0;