Skip to content

2. 与 Web 服务通信的 Android 客户端框架 / jSON

现在我们提供了一个与一个或多个 Web 服务通信的 Android 应用程序框架 / jSON。该项目 [client-android-skel] 位于示例文件夹 [architecture] 中:

  

通过研究这个应用程序骨架,我们将有机会回顾之前示例中遇到的一些要点。 该应用程序将作为所有后续应用程序的框架。它经过多次迭代构建而成。其设计旨在将我们即将构建的应用程序中尽可能多的元素提取到抽象类中,从而避免重复编写仅在细节上有所不同的相同类型代码。其特点如下:

  • 与 Web 服务器 / jSON 的异步通信通过 RxJava 库实现;
  • 片段的生命周期(更新、保存、恢复)由其父类 [AbstractFragment] 管理,该父类会在特定时刻调用其子类的某些方法。因此,子类无需关注生命周期的各个阶段,只需实现父类要求的特定方法即可;
  • 活动的生命周期(保存/恢复)由抽象类 [AbstractActivity] 管理,该类同样要求子活动实现某些方法;
  • 类 [AbstractActivity] 能够管理带或不带标签页、带或不带加载图片、以及是否需要通过 Web 服务器 / jSON 进行基本身份验证的应用程序。这些元素的启用与否通过配置决定;

此框架被用于所有后续示例。由于示例的多样性,适用于一个示例的方法可能不适用于下一个示例。由于该框架总共被用于七个示例,因此经历了多次迭代。 若将其用于第八个示例,该新示例的特殊性可能会再次引发新的错误。尽管如此,使用该模板将极大简化后续示例的编写工作。 事实上,片段的生命周期管理(更新、保存、恢复)与片段邻接性的概念相结合,其复杂程度尤为突出。在此,这些逻辑已被完全封装在 [AbstractFragment] 类中。

2.1. Android客户端架构

所提出的 Android 客户端基于以下架构:

  • [DAO]层实现了[IDao]接口。该层负责与Web服务器/jSON进行通信;
  • 仅有一个 Activity 同时实现了 [IDao] 接口。视图通过该 Activity 访问服务器;
  • 视图由片段实现;

Android 项目反映了这一架构:

  

我们将逐一介绍该项目的各个组成部分。

2.2. 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 {
    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 'io.reactivex:rxandroid:1.2.0'
  compile fileTree(include: ['*.jar'], dir: 'libs')
  testCompile 'junit:junit:4.12'
}

repositories {
  maven {
    url 'https://repo.spring.io/libs-milestone'
  }
}
  • 所有版本号均可能变更。不过,如果将 Android Studio 配置为确保这些 Android 工具版本(第 15-16 行、第 47-48 行)已安装(参见第 6.11 节),则可以基于当前版本号进行操作;

2.3. 应用程序清单

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="client.android">

  <uses-permission android:name="android.permission.INTERNET"/>

  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".activity.MainActivity_"
      android:label="@string/app_name"
      android:windowSoftInputMode="stateHidden"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>
  • 第 3 行:修改应用程序的包名;
  • 第10、15行:需在文件[res / values / strings.xml]中修正[app_name]项的值。目前该值如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>

  <!-- 应用程序名称 -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

2.4. Java代码的组织结构

  
  • [architecture] 汇集了代码组织的主要元素;
  • [activity] 包含应用程序的唯一活动;
  • [fragments] 汇总了应用程序的片段或视图;
  • [dao] 汇总了与 Web 服务器 / jSON 的通信元素;

2.5. 活动组件

 

Image

2.5.1. 与该活动关联的视图

与该活动关联的视图 [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.AppBarLayout>

  <!-- 片段容器 -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
  • 第 29 行:使用了一个特定的片段容器;

该活动还为其视图提供了一个名为 [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">
</menu>

目前该菜单为空。如有需要,开发人员将进行补充。

2.5.2. 片段容器 [MyPager]

  

package client.android.architecture;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

public class MyPager extends ViewPager {

  // 控制滑动
  private boolean isSwipeEnabled;
  // 控制滚动
  private boolean isScrollingEnabled;

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

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

  // 设置器
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }

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

该类仅扩展了 Android 标准类 [ViewPager],用于处理视图间的滑动(第 11 行)和滚动(第 13 行)。

  • 第 26-43 行:用于在滑动功能被禁用时阻止滑动的方法;
  • 第46-49行:重写用于切换显示视图的[setCurrentItem]方法。若已禁用滚动,视图切换将不伴随滚动。 需注意,开发者可通过调用方法 [setCurrentItem(int position, boolean smoothScrolling)] 来绕过此默认行为,该方法允许其指定所需的滚动方式;

2.5.3. 类 [CoreState]

  

类 [CoreState] 是各个片段状态的父类:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// 待办事项:在此添加 [CoreState] 的子类
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // 片段是否已被访问
  protected boolean hasBeenVisited = false;
  // 片段菜单的状态(如有)
  protected MenuItemState[] menuOptionsState;

  // 获取器和设置器
...
}
  • 第16行:每个片段的状态中都包含一个布尔值 [hasBeenVisited],用于标记该片段是否已被访问过。这是必要的,因为有时在首次显示片段时,需要执行一些特殊操作;
  • 第18行:项目[client-android-skel]会自动保存并恢复片段的菜单(如果片段包含菜单)。在数组MenuItemState[] menuOptionsState中,存储了菜单中所有选项的可见状态;
  • 第10-13行:与[Exemple-22]中的做法相同,活动及其片段的状态将保存在会话中,该会话将以字符串jSON的形式保存。 我们将看到,会话存储了一个类型为 [CoreState] 的元素数组。如果不进行任何操作,那么将保存的将是类型为 [CoreState] 的字符串 jSON。 然而,我们希望保存的是片段的状态,即从 [CoreState] 派生出来的状态。为了生成派生类型的字符串 jSON 而不是父类型的字符串,必须按照第 10-13 行所示声明派生类型。 类 [CoreState] 是架构中的一个类,开发人员必须针对每个新应用程序对其进行修改(第 10-13 行);

2.5.4. 接口 [IMainActivity]

  

接口 [IMainActivity] 规定了在以下架构中,片段可以向活动请求的内容:

Image


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // 会话访问
  ISession getSession();

  // 视图切换
  void navigateToView(int position, ISession.Action action);

  // 等待管理
  void beginWaiting();

  void cancelWaiting();

  // 应用程序常量(待修改) -------------------------------------

  // 调试模式
  boolean IS_DEBUG_ENABLED = true;

  // 服务器响应的最大等待时间
  int TIMEOUT = 1000;

  // 执行客户端请求前的等待时间
  int DELAY = 0;

  // 基本身份验证
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // 片段邻接性
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // 标签栏
  boolean ARE_TABS_NEEDED = false;

  // 加载图片
  boolean IS_WAITING_ICON_NEEDED = false;

  // 应用程序片段数量
  int FRAGMENTS_COUNT = 0;

  // 待办事项:在此添加您的常量和其他方法
}
  • 第 6 行:接口 [IMainActivity] 扩展了 [DAO] 层中的接口 [IDao];
  • 第 9 行:该活动以 [ISession] 接口实例的形式提供对会话的访问;
  • 第12行:通过该活动实现视图切换。第二个参数是触发视图切换的操作,其值为SUBMIT、NAVIGATION或RESTORE中的一个;
  • 第15-17行:该Activity负责管理加载动画;
  • 第22行:用于应用程序调试;
  • 第25行:用于在服务器无响应时避免等待过久;
  • 第28行:在调试时,将该值设为几秒钟,以便有时间取消与服务器的操作并查看情况;
  • 第31行:若服务jSON要求基本身份验证,则转至true
  • 第 34 行:片段邻接;
  • 第 37 行:如果应用程序有标签页,则设置为 vrai
  • 第 39 行:若应用程序与 Web 服务器 / jSON 通信,且希望在通信过程中显示加载图片,则设置为 vrai
  • 第 43 行:应用程序管理的片段数量;

接口 [IMainActivity] 是架构中开发者需要填补的第二个元素(第 45 行)。

2.5.5. 接口 [IDao]

接口 [IMainActivity] 扩展了以下接口 [IDao]:

  

package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // Web 服务 URL
  void setUrlServiceWebJson(String url);

  // 用户
  void setUser(String user, String mdp);

  // 客户端超时
  void setTimeout(int timeout);

  // 基本身份验证
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

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

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

  // 待办事项:在此声明您的接口
}
  • 第 24 行:开发人员将在此处完成接口;

2.5.6. 会话

  

类 [Session] 封装了活动和片段共用的元素。它实现了以下接口 [ISession]:


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;

public interface ISession {

  // 上次显示的视图编号
  int getPreviousView();

  void setPreviousView(int numView);

  // 视图的最后状态
  CoreState getCoreState(int numView);

  void setCoreState(int numView, CoreState coreState);

  // 当前操作
  enum Action {
    SUBMIT, NAVIGATION, RESTORE, NONE
  }

  Action getAction();

  void setAction(Action action);

  // 所有视图的状态 -
  // 代码中未使用,但序列化/反序列化时必需 jSON
  CoreState[] getCoreStates();

  void setCoreStates(CoreState[] coreStates);

  // 最后选定标签页的编号
  int getPreviousTab();

  void setPreviousTab(int position);

  // 选中标签页时的导航
  boolean isNavigationOnTabSelectionNeeded();

  void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}

我们引入接口 [ISession],以强制要求会话中包含某些方法:

  • 第 7-10 行:最后显示的视图(片段)编号;
  • 第 12-15 行:特定视图的状态;
  • 第17-24行:引入“正在进行的操作”概念。共有四种(第17行):
    • RESTORE:正在进行保存/恢复操作。视图不会发生变化;
    • NAVIGATION:正在进行导航。此处将“导航”定义为视图切换,且新视图可从会话中存储的最新状态恢复;
    • SUBMIT:当视图发生变更,且新视图不仅取决于其自身状态,还取决于活动整体状态时,将把正在进行的操作类型标记为 [SUBMIT]。 有时,NAVIGATION 与 SUBMIT 难以区分。在这种情况下,应采用更通用的 SUBMIT 类型;
    • NONE:当操作尚未接收其首个值时的操作值;
  • 第26-30行:活动和片段的状态将存储在一个类型为CoreState[]的数组中。为了在jSON的序列化/反序列化过程中正确管理该数组,它必须具有getter和setter方法;
  • 第32-35行:最后选中标签页的编号。在保存/恢复过程中用于重新选中设备旋转前选中的标签页;
  • 第37-40行:管理一个布尔值,用于指示选中标签页时是否应同时切换片段;

接口 [ISession] 由以下抽象类 [AbstractSession] 实现:


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import com.fasterxml.jackson.annotation.JsonIgnore;

public class AbstractSession implements ISession {
  // 上一视图编号
  private int preViousView;

  // 视图状态
  private CoreState[] coreStates = new CoreState[0];

  // 当前操作
  private Action action = Action.NONE;

  // 先前选中的选项卡
  private int previousTab;

  // 通过选定标签页导航
  @JsonIgnore
  private boolean navigationOnTabSelectionNeeded = true;

  // 构造函数
  public AbstractSession() {
    // 初始化片段状态数组
    coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
    for (int i = 0; i < coreStates.length; i++) {
      coreStates[i] = new CoreState();
    }
  }


  // 接口 ISession ---------------------------------------------------------
  @Override
  public int getPreviousView() {
    return preViousView;
  }

  @Override
  public void setPreviousView(int numView) {
    this.preViousView = numView;
  }

  @Override
  public CoreState getCoreState(int numView) {
    return coreStates[numView];
  }

  @Override
  public void setCoreState(int numView, CoreState coreState) {
    coreStates[numView] = coreState;
  }

  @Override
  public Action getAction() {
    return action;
  }

  @Override
  public void setAction(Action action) {
    this.action = action;
  }

  @Override
  public CoreState[] getCoreStates() {
    return coreStates;
  }

  @Override
  public void setCoreStates(CoreState[] coreStates) {
    this.coreStates = coreStates;
  }

  @Override
  public int getPreviousTab() {
    return previousTab;
  }

  @Override
  public void setPreviousTab(int position) {
    this.previousTab = position;
  }

  @Override
  public boolean isNavigationOnTabSelectionNeeded() {
    return navigationOnTabSelectionNeeded;
  }

  @Override
  public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
    this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
  }
}
  • 第 9 行:当前显示视图之前显示的视图编号。当用户可通过多个路径到达某个视图时,此信息非常有用。这在标签页导航中尤为常见。此时,当前显示的视图可以知道之前的视图是什么;
  • 第 12 行:该活动显示的所有片段的状态表;
  • 第18行:先前选中的标签页编号。其作用与第9行中的“前一个视图编号”类似。当设备旋转后需要定位到旋转前选中的标签页时,此信息非常有用;
  • 第22行:一个布尔值,用于指示选择标签页时是否应同时切换显示的片段。需注意,项目[client-android-skel]对标签页和片段进行了独立管理,以便在标签页数量少于片段数量的情况下也能正常使用。 选择分为两种类型:
    • 用户点击标签页时的选择。在此情况下,通常应切换显示的片段;
    • 通过 [Tablayout.Tab.select()] 方法进行的软件选择。在此情况下,并不总是希望显示的片段发生变化。以下是两个示例:
      • 当设备旋转时,活动及标签页都会被重新创建。然而,当第一个标签页创建时,它会自动执行软件操作 [select]。 此时不应更改显示的片段,因为正处于重新创建活动的阶段,最终显示的片段未必是与第一个标签页关联的那个;
      • 由于标签页的管理与片段管理是分离的,因此可能需要在不干扰其关联片段的情况下更新标签页(删除、添加)。 然而,其中某些操作仍可能在某个标签页上触发隐式软件操作 [select]。因此,该选择操作未必会导致导航至关联片段;
  • 第 21 行:字段 [navigationOnTabSelectionNeeded] 无意在保存活动及其片段的操作中被保存。 注释 [@JsonIgnore] 确保该字段在 jSON 的序列化/反序列化过程中被忽略;
  • 第 25-31 行:构造函数初始化应用程序片段的状态数组。该数组的元素通过字段 [FRAGMENTS_COUNT] 进行初始化。此信息用于判断当前是否为该片段的首次访问;

[Session] 类定义如下:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // 片段之间以及片段与活动之间共享的数据
  // 无法在 jSON 中序列化的元素必须带有 @JsonIgnore 注解
  // 切勿遗漏序列化/反序列化所需的 getter 和 setter 方法 jSON
}
  • 第 5 行:类 [Session] 继承了我们刚刚看到的类 [AbstractSession]。开发人员将在其中放置需要在片段之间以及片段与活动之间共享的元素。 需要注意的是,类 [Session] 不再带有注解 AA [@EBean]。它已成为一个普通类;

2.5.7. 抽象类 [AbstractActivity]

  

2.5.7.1. Squelette

类 [AbstractActivity] 是一个超过 300 行代码的类。我们将分阶段进行研究。其骨架如下:


package client.android.architecture;

import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import client.android.R;
import client.android.dao.service.IDao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // [DAO] 层
  private IDao dao;
  // 会话
  protected Session session;

  // 片段容器
  protected MyPager mViewPager;
  // 工具栏
  private Toolbar toolbar;
  // 加载图片
  private ProgressBar loadingPanel;
  // 标签栏
  protected TabLayout tabLayout;

  // 片段或部分管理器
  private FragmentPagerAdapter mSectionsPagerAdapter;
  // 类名
  protected String className;
  // 映射器 jSON
  private ObjectMapper jsonMapper;

  // 构造函数
  public AbstractActivity() {
    // 类名
    className = getClass().getSimpleName();
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "constructeur");
    }
    // jsonMapper
    jsonMapper = new ObjectMapper();
  }

  // 实现 IMainActivity --------------------------------------------------------------------
  ...

  // 生命周期 - 活动备份/恢复 ------------------------------------
  ...

  // 待机画面管理 ---------------------------------
  ...

  // IDao 接口 -----------------------------------------------------
  ...

  // 片段管理器 --------------------------------
  ...

  // 子类
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

  protected abstract int getFirstView();

}

类 [AbstractActivity]:

  • 实现了接口 [IMainActivity](第 21、55 行);
  • 在设备旋转时管理活动及其片段的保存和恢复(第58行);
  • 在与 Web 服务器 / jSON 通信时管理加载画面(第 61 行);
  • 实现 [DAO] 层的 IDao 接口(第 64 行);
  • 实现片段管理器(第 67 行);
  • 要求其子类必须包含六个方法(第 71-81 行);

2.5.7.2. 实现接口 [IMainActivity]

[IMainActivity] 接口的实现(参见第 2.5.4 节)如下:


  // IMainActivity 实现 --------------------------------------------------------------------
  @Override
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position, ISession.Action action) {
    if (IS_DEBUG_ENABLED) {
      Log.d(className, String.format("navigation vers vue %s sur action %s", position, action));
    }
    // 显示新片段
    mViewPager.setCurrentItem(position);
    // 在视图切换时记录当前操作
    session.setAction(action);
}

2.5.7.3. 保存活动及其片段的状态

活动及其片段的状态完全存储在会话中。因此,只需保存该会话即可。此处沿用项目 [Exemple-22](参见第 1.23 节)中的实现:


  // 活动保存/恢复管理 ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // 父级
    super.onSaveInstanceState(outState);
    // 将会话以字符串形式保存jSON
    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();
      }
    }
}

2.5.7.4. 恢复活动及其片段的状态

此处需恢复会话。操作方法如 [Exemple-22] 中所示:


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // 父级
    super.onCreate(savedInstanceState);
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    // 有什么需要恢复的吗?
    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();
        }
      }
    } else {
      // 会话
      session = new Session();
    }
...
  • 第10-26行:如果第2行的参数[Bundle savedInstanceState]不为null,则恢复会话(第12-17行);
  • 第26-29行:若第2行的参数[Bundle savedInstanceState]为null,则表示该活动首次启动。此时将创建一个空会话;

2.5.7.5. 初始化 [DAO] 层


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // 父级
    super.onCreate(savedInstanceState);
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    ...
    // 层[DAO]
    dao = getDao();
    if (dao != null) {
      // 层配置[DAO]
      setDebugMode(IS_DEBUG_ENABLED);
      setTimeout(TIMEOUT);
      setDelay(DELAY);
      setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
    }
...
  // 子类
  protected abstract IDao getDao();
....
}
  • 第 11 行:向子活动(第 21 行)请求 [DAO] 层的引用;
  • 第14-17行:如果[DAO]层存在,则根据[IMainActivity]接口中的信息对其进行配置;

2.5.7.6. 初始化与活动关联的视图

与该活动关联的视图已在第 2.5.1 节中介绍:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context=".activity.MainActivity">

  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>

  <!-- 片段容器 -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>

该视图通过以下代码进行初始化:


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // 父级
    super.onCreate(savedInstanceState);
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // 关联视图
    setContentView(R.layout.activity_main);
    // 视图组件 ---------------------
    // 工具栏
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // 加载图标?
    if (IS_WAITING_ICON_NEEDED) {
      // 添加加载图片
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding loadingPanel");
      }
      // 创建 ProgressBar
      loadingPanel = new ProgressBar(this);
      loadingPanel.setVisibility(View.INVISIBLE);
      // 将 ProgressBar 添加到工具栏
      toolbar.addView(loadingPanel);
    }
...
  • 第 11 行:视图 XML [activity_main] 与该活动相关联;
  • 第 14-15 行:集成并支持工具栏;
  • 第 17-27 行:可能添加加载图片:如果 [IMainActivity] 接口中的布尔值 [IS_WAITING_ICON_NEEDED] 为真;
  • 第23行:创建由字段[loadingPanel]引用的等待图像[ProgressBar];
  • 第 24 行:初始时,该图像处于隐藏状态;
  • 第26行:将其添加到工具栏中;

2.5.7.7. 标签页管理

[IMainActivity] 界面可请求一个标签栏。该标签栏的添加和管理方式如下:


// 标签栏
  protected TabLayout tabLayout;
...

    // 标签栏?
    if (ARE_TABS_NEEDED) {
      // 添加标签栏
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding tablayout");
      }
      // 在显示片段之前不进行选中导航
      session.setNavigationOnTabSelectionNeeded(false);
      // 创建标签栏
      tabLayout = new CustomTabLayout(this);
      tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
      // 将标签栏添加到应用程序栏
      AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
      appBarLayout.addView(tabLayout);
      // 标签栏事件管理器
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
          // 已选中一个标签页
          if (IS_DEBUG_ENABLED) {
            Log.d(className, String.format("onTabSelected n° %s, action=%s, tabCount=%s isNavigationOnTabSelectionNeeded=%s",
              tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
          }
          if (session.isNavigationOnTabSelectionNeeded()) {
            // 标签页位置
            int position = tab.getPosition();
            // 内存
            session.setPreviousTab(position);
            // 显示关联片段?
            navigateOnTabSelected(position);
          }
        }

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

        }

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

        }
      });
    }

...
  // 子类
  protected abstract void navigateOnTabSelected(int position);
...
  • 第 12-48 行:添加并管理标签栏;
  • 第 6 行:若在 [IMainActivity] 接口中将常量 [ARE_TABS_NEEDED] 设置为 vrai,则添加标签栏;
  • 第 12 行:在创建标签栏时,可能会发生隐式操作 [Tablayout.Tab.select](并非由用户触发)。 将布尔值 [session.navigationOnTabSelectionNeeded] 设置为 faux,以避免在这些虚假选择期间进行导航。开发者需通过方法 [navigateToView] 选择要显示的片段。 当该片段显示时,布尔值 [session.navigationOnTabSelectionNeeded] 将重置为 vrai(参见类 AbstractFragment);
  • 第 14 行:创建一个由字段 [tabLayout] 引用的标签栏。我们使用了一个自定义标签栏 [CustomTabLayout],稍后将对此进行说明;
  • 第 15 行:我们设置标签标题的颜色。这些颜色可在以下文件 [res / color / tab_txt.xml] 中找到:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_selected="true" android:color="#FFFF00" />
  <item android:state_selected="false" android:color="#FFFFFF" />
</selector>
    • 第 (c) 行:选中标签页时标签标题的颜色;
    • 第 (d) 行:标签未被选中时的标题颜色;

该文件当然可以修改。例如,可以在此处找到颜色的十六进制代码。

  • 第 17-18 行:将此标签栏添加到视图 XML [activity_main] 中的应用程序栏中;
  • 第20-47行:标签栏事件处理程序;
  • 第22-36行:仅处理[onTabSelected]事件。该事件对应于点击作为方法参数传递的[Tab tab]标签,或执行[TabLayout.Tab.select]软件操作;
  • 第 30 行:所选标签页的位置;
  • 第 32 行:该位置被存储在会话中;
  • 第 34 行:现在需要显示与该标签页关联的片段。只有子类(第 52 行)才能进行此关联。需要注意的是,我们并未像某些已研究的示例那样,将标签栏与片段容器 [mViewPager] 进行关联。 在此,我们完全将标签栏的管理与片段的管理分离。因此,当点击某个标签时,必须指定要显示的视图;
  • 第28行:区分了带导航和不带导航的标签选择。 通常,当用户点击标签页时,需要导航功能;而在软件选择时则不需要。开发者需通过 [session.navigationOnTabSelectionNeeded] 元素区分这两种情况。当不进行导航时,最后选定标签页的编号不会保存在会话中。这需要由开发者自行处理

2.5.7.8. 标签页管理器 [CustomTabLayout]

  

我们使用自定义标签页管理器,以便能够使用不同的字体显示标签页标题。类 [CustomTabLayout] 如下所示:


package client.android.architecture.custom;

import android.content.Context;
import android.graphics.Typeface;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class CustomTabLayout extends TabLayout {
  private Typeface mTypeface;

  public CustomTabLayout(Context context) {
    super(context);
    init();
  }

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

  public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
  }

  private void init() {
    mTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/Roboto-Bold.ttf");
  }

  @Override
  public void addTab(Tab tab) {
    super.addTab(tab);

    ViewGroup mainView = (ViewGroup) getChildAt(0);
    ViewGroup tabView = (ViewGroup) mainView.getChildAt(tab.getPosition());

    int tabChildCount = tabView.getChildCount();
    for (int i = 0; i < tabChildCount; i++) {
      View tabViewChild = tabView.getChildAt(i);
      if (tabViewChild instanceof TextView) {
        ((TextView) tabViewChild).setTypeface(mTypeface, Typeface.NORMAL);
      }
    }
  }

}
  • 标签标题字体的自定义设置位于第 30 行和第 44 行;

文件 [fonts] 内容如下:

  

来源

  • 在URL和[http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout]中找到了[CustomTabLayout]类的代码;
  • 字体位于 URL 和 [https://www.fontsquirrel.com/fonts/roboto] 中;

2.5.7.9. 最近的初始化


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // 父类
    super.onCreate(savedInstanceState);
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // 片段管理器的实例化
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // 片段容器与片段管理器相关联
    // 即片段容器中的第 i 个片段是片段管理器提供的第 i 个片段
    mViewPager = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // 禁止片段间的滑动操作
    mViewPager.setSwipeEnabled(false);
    // 片段的相邻性
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    // 显示第一个视图
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    // 将控制权移交给子活动
    onCreateActivity();
  }
...
  // 子类
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • 第10-19行:此处包含我们在已研究示例中经常遇到的代码;
  • 第21-23行:显示第一个视图。区分这种情况的方法可能有多种。在此,我们利用了以下事实:对于第一个视图,触发视图切换的操作值为NONE;
  • 第22行:我们不对要显示的第一个片段做任何假设。在我们的示例中,这通常是第0号片段,但并非总是如此(参见示例-22)。因此,我们将要求子活动(第30行)告知我们这个第一个视图是什么;
  • 第25行:此处已尽可能地进行了代码提取。现在,子类需要执行自己的初始化操作(第29行);

2.5.7.10. 加载图片管理

在类 [AbstractActivity] 中,加载图片由以下两个方法管理:


  // 加载图管理 ---------------------------------
  public void cancelWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.INVISIBLE);
    }
  }

  public void beginWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.VISIBLE);
    }
}

2.5.7.11. 接口 [IDao] 的实现

在类 [AbstractActivity] 中,接口 [IDao](参见第 2.5.5 节)的实现如下:


public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // 层 [DAO]
  private IDao dao;
...
  // IDao 接口 -----------------------------------------------------
  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    dao.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    dao.setTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    dao.setBasicAuthentification(isBasicAuthentificationNeeded);
  }

  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    dao.setDebugMode(isDebugEnabled);
  }

  @Override
  public void setDelay(int delay) {
    dao.setDelay(delay);
}
  • 第 3 行:需注意该字段的值是由子活动在方法 [onCreate] 中提供的;

2.5.7.12. 片段管理器的实现

在类 [AbstractActivity] 中,片段管理器的实现如下:


...
  // 片段管理器 --------------------------------
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    private AbstractFragment[] fragments;

    // 构造函数
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
      // 子类的片段
      fragments = getFragments();
    }

    // 需渲染第 n 号片段
    @Override
    public AbstractFragment getItem(int position) {
      // 渲染片段
      return fragments[position];
    }

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

    // 返回第 n 位片段的标题
    @Override
    public CharSequence getPageTitle(int position) {
      return getFragmentTitle(position);
    }
  }

  // 子类
  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • 第 5 行:与该活动关联的片段数组。所有片段都将继承自 [AbstractFragment] 类;
  • 第 8-12 行:这是初始化片段数组的构造函数。它从活动的子类(第 35 行)中获取这些片段;
  • 第28-31行:片段标题可用于片段数量与标签页数量相等的应用程序中。在此情况下,可将片段标题赋予标签页。此处,这些标题是从子类中获取的(第37行);

2.5.7.13. 方法 [onResume]

方法 [onResume] 会在与该活动关联的视图显示之前稍早执行。此处使用该方法是在保存/恢复操作后选择一个选项卡:


  @Override
  public void onResume() {
    // 父级
    super.onResume();
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onResume");
    }
    // 若需恢复,则需恢复最后选中的标签页
    if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
      tabLayout.getTabAt(session.getPreviousTab()).select();
    }
}
  • 第 10 行:选择在保存/恢复过程之前所选中的标签页。 需要注意的是,在 [onCreate] 方法中(该方法在业务流程的生命周期中位于 [onResume] 方法之前),已禁用了通过选择标签页进行的导航。因此,此处虽然选择了标签页,但不会切换片段;

2.5.7.14. Résumé

抽象类 [AbstractActivity] 将作为应用程序中唯一活动的父类。

子活动必须实现以下六个方法:


  // 子类
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

protected abstract int getFirstView();

此外,子活动还可以访问其父类的以下受保护成员:


  // 会话
  protected ISession session;
  // 片段容器
  protected MyPager mViewPager;
  // 标签栏
  protected CustomTabLayout tabLayout;
  // 类名
protected String className;

2.5.8. [MainActivity] 活动

  

类 [MainActivity] 可以有不同的名称。其唯一的要求是实现接口 [IMainActivity]。提供的基类如下:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // 层[DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // 会话
  private Session session;

  // 父类方法 -----------------------
  @Override
  protected void onCreateActivity() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // 会话
    this.session = (Session) super.session;
    // 待办事项:继续父类已开始的初始化工作
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // 待办事项:在此处定义片段
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // 待办事项:在此处定义片段标题
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // 待办事项:标签页导航——定义要显示的视图
  }

  @Override
  protected int getFirstView() {
    // 待办事项:标签页导航 - 定义要显示的首个视图
    return 0;
  }
}
  • 第 14 行:为了使第 19 行中的 AA [@Bean] 标记被理解,该活动必须具有 AA [@EActivity] 标记;
  • 第15行:该活动关联到菜单 XML [menu_main]。目前该菜单为空。开发人员如有需要,需自行补充;
  • 第 16 行:该类继承自类 [AbstractActivity];
  • 第19-20行:对[DAO]层的引用。该层将在该字段初始化之前由AA库实例化。 这意味着 AA 和 [Dao] 两个 Bean 必须存在。 在我们提供的模板应用程序中,情况始终如此。即使在没有 [DAO] 层的应用程序中,也可以保留 [dao] 包。这不会带来任何复杂性;
  • 第 22 行:会话作为类型 [Session] 的实例。该会话存在于父类 [AbstractActivity] 中,但作为接口 [ISession] 的实例(第 32 行);
  • 第24-63行:父类[AbstractActivity]强制要求的六个方法;
  • 第36-39行:方法[getDao]返回对层[DAO]的引用。在此处,该引用从未是null。 然而在父类 [AbstractActivity] 中,已预设了子类返回引用 null 的情况,以表示不存在层 [DAO]。 如果希望使用这一功能(我认为并不太实用),则应在此处返回指针 null

2.6. [DAO] 层

Image

  

2.6.1. 接口 IDao

该接口已在第 2.5.5 节中介绍:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // Web 服务的 URL
  void setUrlServiceWebJson(String url);

  // 用户
  void setUser(String user, String mdp);

  // 客户端超时
  void setTimeout(int timeout);

  // 基本身份验证
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

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

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

  // 待办事项:在此声明您的接口
}

开发人员将从第24行开始添加其[DAO]层的方法。

2.6.2. 接口 [WebClient]

  

接口 [WebClient] 如下所示:


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // 待办事项:在此声明要实现的 URL
}

开发人员将从第 17 行开始添加与 jSON 服务器公开的 URL 进行通信的方法。

2.6.3. 身份验证拦截器 [MyAuthInterceptor]

  

[MyAuthInterceptor] 类如下:


package client.android.dao.service;

import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;

@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {

  // 用户
  private String user;
  // 密码
  private String mdp;

  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // 被拦截的 HTTP 请求的 HTTP 标头
    HttpHeaders headers = request.getHeaders();
    // 基本身份验证的 HTTP 标头
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    // 添加到 HTTP 请求头中
    headers.setAuthorization(auth);
    // 继续处理 HTTP 请求的生命周期
    return execution.execute(request, body);
  }

  // 身份验证元素
  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}

该类生成以下认证头:HTTP

Authorization: Basic code

其中 [code] 是字符串 'user:mp' 的 Base64 编码。该类仅在 jSON 服务器期待此种认证形式时才起作用。此外还有其他形式。

:该类的用法在第 3.6.3.1 节中进行了说明。

2.6.4. 类 [AbstractDao]

  

[AbstractDao]类如下:


package client.android.dao.service;

import android.util.Log;
import client.android.architecture.core.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;

public abstract class AbstractDao {

  // 映射器 jSON
  private ObjectMapper mapper = new ObjectMapper();
  // 调试模式
  protected boolean isDebugEnabled;
  // 类名
  protected String className;
  // 执行请求前的等待延迟
  private int delay;

  // 构造函数
  public AbstractDao() {
    // 类名
    className = getClass().getName();
    Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
  }

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

  // 向 Web 服务发送的泛型请求 / jSON
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // 日志
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("delay=%s", delay));
    }
    // 服务执行 - 等待唯一响应
    return Observable.create(new Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // 服务执行
        try {
          // 等待中?
          if (delay > 0) {
            Thread.sleep(delay);
          }
          // 正在执行同步请求
          T response = request.getResponse();
          // 日志
          if (isDebugEnabled) {
            String log;
            if (response instanceof String) {
              log = (String) response;
            } else {
              log = mapper.writeValueAsString(response);
            }
            Log.d(className, String.format("response=%s sur thread [%s]", log, Thread.currentThread().getName()));
          }
          // 向观察者发送响应
          subscriber.onNext(response);
          // 报告可观察对象结束
          subscriber.onCompleted();
        } catch (InterruptedException | JsonProcessingException | RuntimeException e) {
          // 日志
          if (isDebugEnabled) {
            try {
              Log.d(className, String.format("Thread [%s], Exception communication avec serveur : %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
            } catch (JsonProcessingException e1) {
              Log.d(className, String.format("Erreur jSON imprévue"));
            }
          }
          // 抛出异常
          subscriber.onError(new DaoException(e, 100));
        }
      }
    });
  }

  // 调试模式
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
  }

  public void setDelay(int delay) {
    this.delay = delay;
  }
}
  • 第 35-81 行:方法 [getResponse] 使用库 RxAndroid 来渲染类型 [Observable<T>]。 与之前看到的某些示例不同,这里渲染的不是专有类型 [Response<T>],而是任意 T 类型;
  • 第 35 行: 方法 [getResponse] 接收第 30-32 行中 [IRequest<T>] 类型的实例作为参数,该实例通过同步操作 HTTP 由方法 [IRequest.getReponse()] 获取类型 T;
  • 第48-50行:人为设置等待时间为[delay]毫秒。在生产环境中将设置为[delay=0]。 在调试阶段,将设置为 [delay=qqs secondes],以便用户有机会取消异步操作,从而观察代码此时的行为;
  • 第 52 行:通过同步请求获取预期的响应;
  • 第 64 行:收到响应后,将其传递给观察者;
  • 第 66 行:声明不再有新数据发布。此处属于仅返回一个元素的异步操作特例;
  • 第67-78行:若发生异常,将异常抛出给观察者(第77行);

2.6.5. 类 [Dao]

  

类 [Dao] 如下:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // Web 服务客户端
  @RestService
  protected WebClient webClient;
  // 安全性
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  // RestTemplate 的工厂
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // 日志
    Log.d(className, "afterInject");
    // 正在构建 restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // 安装 jSON 转换器
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // 设置 Web 客户端的 restTemplate
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // 设置 Web 服务的 URL
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // 在拦截器中注册用户
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // 配置工厂
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // 身份验证拦截器?
    if (isBasicAuthentificationNeeded) {
      // 添加身份验证拦截器
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // 私有方法 -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // 待办事项:实现 IDao
}
  • 第21-22行:注入Bean AA [WebClient],该Bean将负责与Web服务器/jSON进行通信;
  • 第 24-25 行:注入身份验证拦截器;
  • 第31-42行:在注入第21-25行的字段后执行的方法;
  • 第37行:用于处理客户端/服务器通信的对象[RestTemplate]是从factory创建的。 虽然并非必须,但正是通过 factory 才能配置通信超时。因此,我们不使用无参数构造函数 [RestTemplate()];
  • 第 39 行:将转换器 jSON 添加到 [RestTemplate] 的转换器列表中。这将是唯一的转换器。 此外,当客户端方法 [WebClient] 接收来自服务器的字符串 jSON 时,该字符串将自动反序列化为该方法应返回的对象;
  • 第41行:如此配置的[RestTemplate]对象被传递给Web客户端,客户端将借助该对象处理客户端与服务器之间的交互;
  • 第 44-48 行:将 Web 服务器的根 URL 设置为 jSON。 在类 [WebClient] 中声明的所有 URL 都是相对于该根 URL 的 URL;
  • 第50-54行:当连接受基本授权类型控制时(参见第2.6.3节),此方法用于指定连接的所有者;
  • 第56-64行:定义客户端/服务器通信的timeouts。这是通过管理通信的[RestTemplate]对象的factory实现的;
  • 第 66-78 行:此方法用于指定服务器受基本身份验证保护;
  • 第 72-77 行:如果要求基本身份验证,则将第 25 行注入的身份验证拦截器添加到 [RestTemplate] 对象的拦截器中。 该拦截器将自动在所有 Web 客户端请求中添加服务器所期望的基本身份验证行 HTTP;
  • 开发人员将从第 87 行开始实现 [IDao] 接口;

2.7. 片段

  

2.7.1. [MenuItemState] 类

类 [MenuItemState] 封装了菜单选项的状态:


package client.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;
  }

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

2.7.2. 类 [Utils]

类 [Utils] 集合了静态实用方法:


package client.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 getMessageForAlert(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();
  }

  // 异常消息列表 - 版本 3
  static public String getMessageForAlert(List<String> messages) {
    // 构建要显示的文本
    StringBuilder texte = new StringBuilder();
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // 结果
    return texte.toString();
  }
}

2.7.3. 父类 [AbstractFragment]

类 [AbstractFragment] 汇集了应用程序所有片段的公共部分。与类 [AbstractActivity] 一样,其代码较为复杂。我们也将分步骤对其进行分析。

2.7.3.1. 框架


package client.android.architecture.core;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

import java.util.ArrayList;
import java.util.List;

public abstract class AbstractFragment extends Fragment {

  // 私有数据 ------------------------------------------------------------
  // 可观察对象的订阅
  private List<Subscription> abonnements = new ArrayList<>();
  // 片段菜单
  private Menu menu;
  private MenuItemState[] menuOptionsStates = new MenuItemState[0];
  // 片段生命周期
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // 片段状态
  private CoreState previousState;
  // 映射器 jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // 片段生命周期
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
  // 异步任务
  private boolean runningTasksHaveBeenCanceled;

  // 子类可访问的数据 ---------------------------------------
  // 调试模式
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // 类名
  protected String className;
  // 异步任务
  protected int numberOfRunningTasks;
  // 活动
  protected IMainActivity mainActivity;
  protected Activity activity;
  // 会话
  protected Session session;


  // 更新片段 ----------------------------------------------------------------------------------
 ...

  // 菜单管理 ------------------------------------------
  ...

  // 等待管理 -------------------------------------------------------------
...

  // 异步操作管理 --------------------------------------------------------------------
...

  // 异常处理 -------------------------------------------------------------------
....

  // 片段生命周期管理 --------------------------------------------------------
...

  // 子类 -----------------------------------------------------
  public abstract CoreState saveFragment();

  protected abstract int getNumView();

  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();

  protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);

}
  • 第28-45行:类的私有数据;
  • 第47-58行:子类可访问的受保护数据;
  • 第61-62行:用于更新待显示片段的代码;
  • 第64-65行:用于管理可能出现的菜单的辅助代码;
  • 第 67-68 行:用于管理异步操作期间等待状态的辅助代码;
  • 第 70-71 行:用于简化片段与 [DAO] 层通信的代码;
  • 第 73-74 行:用于以标准方式处理所有异常的辅助代码;
  • 第 76-77 行:管理片段生命周期的代码;
  • 第80-94行:父类为其子类强制定义了8个方法;

2.7.3.2. 构造函数

该类的构造函数如下:


  // 类名
  protected String className;
  // 片段生命周期
  private boolean fragmentHasToBeInitialized = false;
...
  // 构造函数 ----------------------
  public AbstractFragment() {
    // 初始化
    className = getClass().getSimpleName();
    fragmentHasToBeInitialized = true;
    // 日志
    if (isDebugEnabled) {
      Log.d(className, "constructeur");
    }
}
  • 第 9 行:记录了此处实例化的子类名称。该名称将用于父类的所有日志中;
  • 第10行:记录片段正在被构建。当要求子片段更新时,将使用此信息;

2.7.3.3. 菜单管理

在我们的架构中,所有片段都必须拥有一个菜单,即使该菜单为空。日志显示,当片段拥有菜单时,方法 [onCreateOptionsMenu] 执行之际,该片段已与其活动、视图及菜单关联,并即将显示。因此,此时正是更新视觉界面和菜单的时机。 正是在该方法 [onCreateOptionsMenu] 中,我们要求子片段进行更新。

菜单管理包含一系列实用方法,用于控制子片段是否显示菜单项:


  // 片段菜单
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
...
  // 菜单管理 ------------------------------------------
  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);
      }
    }
  }

  private void getMenuOptionsStates(Menu menu) {
    // 结果
    if (isDebugEnabled) {
      Log.d(className, "getMenuOptionsStates(Menu)");
    }
    // 获取菜单选项的标识符
    List<Integer> menuOptionsIds = new ArrayList<>();
    getMenuOptions(menu, menuOptionsIds);
    // 将菜单选项转移到数组中
    menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // 选项标识符
      int id = menuOptionsIds.get(i);
      // 选项状态
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // 结果
    if (isDebugEnabled) {
      Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
    }
  }

  // 菜单选项的状态
  private MenuItemState[] getMenuOptionsStates() {
    MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // 状态
      MenuItemState state = this.menuOptionsStates[i];
      // 菜单 ID
      int id = state.getMenuItemId();
      // 状态初始化
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // 结果
    return menuOptionsStates;
  }

  // 显示菜单选项 -----------------------------------
  protected void setAllMenuOptionsStates(boolean isVisible) {
    // 更新所有菜单选项
    for (MenuItemState menuItemState : menuOptionsStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
    }
  }

  protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
    // 更新部分菜单选项
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}
  • 第 6-18 行:该方法用于获取菜单中所有选项的数字标识符;
  • 第 6 行:方法 [getMenuOptions] 接收两个参数:
    • [Menu menu]:片段的菜单;
    • [List<Integer> menuOptionsIds]:菜单选项的 Android 标识符列表。初始时该列表为空。随后通过递归遍历(第 15 行)菜单树来填充该列表;
  • 第20-40行:基于菜单,构建菜单选项的状态数组(ID、可见性)。该数组存储在第3行。类[MenuItemState]已在第2.7.1节中描述;
  • 第43-55行:前一种方法的变体。其功能相同,但无需重新计算菜单中所有选项的标识符(该操作已完成),而是直接使用第3行状态数组中的标识符;
  • 第58-63行:方法[setAllMenuOptionsStates]可用于隐藏或显示片段菜单中的所有选项;
  • 第 65-69 行:方法 [setMenuOptionsStates] 允许有选择地显示或隐藏菜单中的某些选项;
  • 方法 [getMenuOptions, getMenuOptionsStates] 被声明为私有,因为它们仅在 [AbstractFragment] 中使用。 方法 [setAllMenuOptionsStates](第 58 行)和 [setMenuOptionsStates](第 65 行)被声明为受保护的,以便子类可以使用;

2.7.3.4. 异步任务结束的等待管理


   // 可观察对象的订阅
  private List<Subscription> abonnements = new ArrayList<>();
// 异步任务
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...

  // 管理异步操作结束的等待 -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // 记录即将执行的任务数量
    this.numberOfRunningTasks = numberOfRunningTasks;
    // 设置等待图标
    mainActivity.beginWaiting();
    // 清空订阅列表
    abonnements.clear();
    // 尚未取消
    runningTasksHaveBeenCanceled = false;
  }

  protected void cancelWaitingTasks() {
    // 隐藏加载图片
    mainActivity.cancelWaiting();
  }

  • 第9-18行:为了启动一个或多个异步操作,子片段将调用父方法[beginRunningTasks]。该方法的参数是子片段将启动的异步任务数量;
  • 第 11 行:保存该方法的参数;
  • 第 13 行:显示加载图标;
  • 第 15 行:清空异步操作的订阅列表。这些操作尚未由子片段创建;
  • 第17行:维护一个布尔变量,用于标记子片段请求的异步任务已被取消。该布尔变量的初始值为false
  • 第20-25行:子片段调用父方法[cancelWaitingTasks],以表明其希望取消已启动的任务;
  • 第 22 行:隐藏了等待图像;

2.7.3.5. 异常处理


  // 异常处理 -------------------------------------------------------------------

  // 显示异常警报
  protected void showAlert(Throwable th) {
    // 显示 Throwable th 的异常堆栈信息
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
  }

  // 显示消息列表
  protected void showAlert(List<String> messages) {
    // 显示消息列表
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
  • 第 4-7 行:方法 [showAlert(Throwable)] 允许子片段在窗口中显示作为参数传递的 Throwable 的异常堆栈消息;
  • 第10-13行:方法[showAlert(List<String>]允许子片段在窗口中显示作为参数传递的消息列表;
  • 第 6 行和第 12 行中使用的 [Utils] 类已在第 2.7.2 节中描述;

2.7.3.6. 异步操作管理


...
  // 可观察对象的订阅
  private List<Subscription> abonnements = new ArrayList<>();
  // 异步任务
  private boolean runningTasksHaveBeenCanceled;
  protected int numberOfRunningTasks;
...
  // 使用 RxAndroid 执行异步任务
  protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
    // process:待执行/监听的可观察对象
    // consumeResult:处理所获响应的方法
    // 
    // 仅在未发生取消时才创建新的订阅
    if (!runningTasksHaveBeenCanceled) {
      // 在 I/O 线程上执行,并在 UI 线程上进行观察
      process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
      // 执行可观察对象
      try {
        abonnements.add(process.subscribe(
          // 消费结果
          consumeResult,
          // 处理异常
          new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
              consumeThrowable(th);
            }
          },
          // 任务结束
          new Action0() {

            @Override
            public void call() {
              endOfTask();
            }
          }));
      } catch (Throwable th) {
        consumeThrowable(th);
      }
    }
  }

  private void endOfTask() {
...
  }

  // 某项异步操作抛出了异常
  // 或在执行异步操作期间发生异常
  private void consumeThrowable(Throwable th) {
...
  }

  • 第 9-41 行:执行一项异步任务;
  • 第 9 行:方法 [executeInBackground] 需要两个参数:
    • [Observable<T> process]:待执行的异步进程;
    • [Action1<T> consumeResult]:要调用的子片段方法,用于向其传递进程发出的元素。在之前的示例中,进程始终只发出一个元素。[Action1<T>] 的类型 T 是被观察进程返回结果的类型 T;
  • 第 14 行:仅当尚未发生用户取消或程序(因异常)取消时,才启动异步任务;
  • 第 16 行:该进程配置为在 I/O 线程上运行,并在 UI 线程上进行观察;
  • 第 16 行:指令 [process.subscribe] 在 I/O 线程中启动进程的执行。在此线程内部,操作以同步方式执行,因为我们使用的库 HTTP 是同步的;
  • 第 19 行:方法 [process.subscribe] 有三个参数:
    • 第 21 行:[consumeResult]:子片段的方法,将处理进程发出的元素;
    • 第 22-28 行:当异步任务处理过程中发生异常时执行的方法。处理工作委托给第 49 行的 [consumeThrowable] 方法;
    • 第29-36行:任务发出发送完成通知时执行的方法。处理过程委托给第43行的[endOfTask]方法;
  • 第 19 行:刚刚启动的异步任务被记录在字段 [abonnements] 中,该字段用于记录所有已启动的异步任务。这将允许在必要时取消这些任务;
  • 第37-39行:当处理异步任务时发生异常时执行的方法。处理过程委托给第49行的[consumeThrowable]方法;

方法 [endOfTask] 如下:


  // 异步任务
  protected int numberOfRunningTasks;
...
  private void endOfTask() {
    // 少了一项待处理的任务
    numberOfRunningTasks--;
    // 结束了吗?
    if (numberOfRunningTasks == 0) {
      // 等待结束
      cancelWaitingTasks();
      // 向子类报告任务结束
      notifyEndOfTasks(false);
    }
  }
...
  // 子类 -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • 第 6 行:一个异步任务刚刚完成。将活动任务计数器减 1;
  • 第8行:如果不再有活动任务,则子片段已获取所有响应;
  • 第 10 行:取消等待;
  • 第 12 行:通过调用子片段的 [notifyEndOfTasks] 方法,通知子片段其启动的所有任务均已结束。该方法的参数指示任务结束的方式,包括正常结束、用户取消或因发生异常而由代码取消。第 12 行报告的是正常结束。 需要注意的是,子片段无需担心跟踪仍处于活动状态的任务。其父类会代为处理;

方法 [consumeThrowable] 如下所示:


  // 异步任务
  protected int numberOfRunningTasks;
  private boolean runningTasksHaveBeenCanceled;
...
    // 某项异步操作抛出了异常
  // 或在执行异步操作期间发生了异常
  private void consumeThrowable(Throwable th) {
    // th:待处理的异常
    // 
    // 日志
    if (isDebugEnabled) {
      Log.d(className, "Exception reçue");
    }
    // 已启动的任务被取消
    cancelRunningTasks();
    // 显示错误消息
    showAlert(th);
  }

  // 取消任务
  protected void cancelRunningTasks() {
    // 日志
    if (isDebugEnabled) {
      Log.d(className, "Annulation des tâches lancées");
    }
    // 取消所有已注册的异步任务
    for (Subscription abonnement : abonnements) {
      abonnement.unsubscribe();
    }
    // 记录取消操作
    runningTasksHaveBeenCanceled = true;
    numberOfRunningTasks = 0;
    // 等待结束
    cancelWaitingTasks();
    // 向子片段报告任务已取消
    notifyEndOfTasks(true);
}

...
  // 子类 -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • 第 3 行:方法 [consumeThrowable] 接收发生的异常;
  • 第 15 行:所有仍在运行的任务均被取消;
  • 第 17 行:显示异常文本;
  • 第 21-37 行:取消所有任务;
  • 第 27-29 行:取消所有订阅;
  • 第 31 行:记录已发生取消;
  • 第32行:将任务计数器重置为零;
  • 第34行:取消等待;
  • 第36行:向子片段报告取消操作后任务结束;

2.7.3.7. 片段生命周期管理


  // 生命周期 --------------------------------------------------------
  @Override
  public void onDestroyView() {
    // 父级
    super.onDestroyView();
    // 日志
    if (isDebugEnabled) {
      Log.d(className, "onDestroyView");
    }
  }

  @Override
  public void onDestroy() {
    // 父级
    super.onDestroy();
    // 日志
    if (isDebugEnabled) {
      Log.d(className, "onDestroy");
    }
  }

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

  private void saveState() {
...
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
...
  }


  @Override
  public void onSaveInstanceState(final Bundle outState) {
...
}
  • 第2-20行:方法[onDestroyView, onDestroy]仅用于日志记录。这些日志有助于开发者更好地理解片段的生命周期;

设备旋转时片段的保存由以下 [setUserVisibleHint, onSaveInstanceState, saveState] 方法实现:


  // 片段生命周期
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
...

@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // 父级
    super.setUserVisibleHint(isVisibleToUser);
    // 保存?
    if (this.isVisibleToUser && !isVisibleToUser) {
      // 片段即将被隐藏 - 正在保存
      if (!saveFragmentDone) {
        saveState();
      }
    }
    // 内存
    this.isVisibleToUser = isVisibleToUser;
  }

  private void saveState() {
...
  }

  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // 日志
    if (isDebugEnabled) {
      Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
    }
    // 父级
    super.onSaveInstanceState(outState);
    // 仅当片段可见时才保存
    if (isVisibleToUser) {
      // 可能已经进行了备份
      if (!saveFragmentDone) {
        saveState();
      }
      // 无论如何都需要进行恢复
      session.setAction(ISession.Action.RESTORE);
    }
}
  • 第 6-19 行:当片段状态从显示状态切换为隐藏状态时(第 11 行),将执行片段保存操作。该信息由方法 [setUserVisibleHint] 提供;
  • 第 14 行:保存操作由第 21-23 行的私有方法执行;
  • 第25-41行:当设备旋转时,将调用方法[onSaveInstanceState]。在以下两种情况下会保存片段:
    • 该片段处于可见状态(第34行);
    • 该片段尚未被保存(第36行)。当片段可见时,[setUserVisibleHint, onSaveInstanceState]这两个方法可能无法同时执行,因此对布尔值[saveFragmentDone]的管理可能毫无意义。出于谨慎,我还是选择了使用该布尔值;
  • 第40行:保存之后将进行恢复。需要注意的是,下次片段需要更新时,必须通过[RESTORE]操作进行;

需注意片段被要求保存的两个时刻:

  1. 当片段状态从可见切换为隐藏时;
  2. 当设备发生旋转时;

私有方法 [saveState] 如下:


...
  private void saveState() {
    // 需要取消的任务?
    if (numberOfRunningTasks != 0) {
      // 取消任务
      cancelRunningTasks();
    }
    // 正在备份片段的状态
    CoreState currentState = saveFragment();
    // 已访问该片段
    currentState.setHasBeenVisited(true);
    // 保存菜单状态
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // 登录
    session.setCoreState(getNumView(), currentState);
    // 保存完成
    saveFragmentDone = true;
    // 日志
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }


...
  // 子类 -----------------------------------------------------
public abstract CoreState saveFragment();

protected abstract int getNumView();
  • 第4-7行:在异步操作进行期间,设备可能会发生旋转。在此情况下,系统决定取消所有这些操作。 这对用户来说并非明智之举——用户可能仅仅是移动了手机或平板电脑,或者接到了一个电话,却不得不重新发起一个可能耗时较长的请求。虽然可以通过备份/恢复周期来保留网络连接,但解决方案并不显而易见,因此我决定不在本入门课程中探讨这些内容。 正确的做法是通过一个不带视觉界面、且在备份/恢复周期中不会被销毁的片段来建立这些网络连接。只需使用指令 [Fragment.setRetainInstance(true)] 即可;
  • 第 9 行:要求子片段将其状态保存到 [CoreState] 的派生类型中(第 31 行);
  • 第 11 行:记录该片段已被访问。此信息非常有用。当片段首次被访问时,其更新操作可能与后续访问不同,因为此时该片段在会话中尚无历史状态;
  • 第 13 行:保存菜单的状态,以便后续自动恢复;
  • 第15行:当前状态被保存到会话中。在会话中,状态按视图/片段分组,每个视图/片段对应一个状态。视图编号由子片段提供(第33行);
  • 第17行:记录片段已保存。这是因为有两个方法可能会调用[saveState]方法,而重复保存是没有必要的;

与片段关联的视图的重新生成由以下方法负责:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // 父类
    super.onActivityCreated(savedInstanceState);
    // 日志
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // 视图需恢复
    viewHasToBeInitialized = true;
}

在生命周期中,方法 [onActivityCreated] 紧随方法 [onCreateView] 之后执行。调用后者表明需重建与片段关联的视图。我们仅在第 10 行对此进行标注。

2.7.3.8. 片段更新

片段更新是片段在显示出来并等待用户操作之前进行的最后一步操作。该操作由以下代码实现:


  // 片段菜单
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // 片段生命周期
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // 片段状态
  private CoreState previousState;
  // 映射器 jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // 片段生命周期
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...

  // 更新片段 ----------------------------------------------------------------------------------
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // 日志
    if (isDebugEnabled) {
      Log.d(className, "onCreateOptionsMenu");
    }
    // 内存
    this.menu = menu;
    // 如果尚未执行,则获取菜单中的 # 选项
    if (fragmentHasToBeInitialized) {
      // 获取菜单中的 # 选项
      getMenuOptionsStates(menu);
      // 活动
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = (Session) this.mainActivity.getSession();
    }
    // 获取片段的先前状态(首次执行时,仅布尔值 hasBeenVisited 具有实际意义)
    previousState = session.getCoreState(getNumView());
    // 分多步更新子片段
    // 步骤 1 - 是否为首次访问?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
  ...
    } else {
      // 不是首次访问
      // 步骤 2:是否需要初始化片段?
      ...
      // 步骤 3:视图是否需要初始化?
      ...
    }
    // 步骤 4:提交、导航还是恢复?
    ...

    // 步骤 5:终端更新 ----------------------
...
  }
...
  // 子类 -----------------------------------------------------
  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();
  • 第19行:使用方法[onCreateOptionsMenu]来更新片段。因此,片段必须拥有一个菜单(如有需要,该菜单可以为空)。当此方法执行时,片段已与其视图和活动关联,并且处于可见状态;
  • 第 25 行:保存作为参数(第 22 行)传递给该方法的菜单;
  • 第 27-34 行:如果片段需要初始化:
    • 第 29 行:将菜单选项的状态存入第 3 行定义的 [menuOptionsStates] 数组中;
    • 第 31 行:将该活动作为 Android 类型 [Activity] 的实例进行存储;
    • 第 32 行:将 Activity 作为 [IMainActivity] 接口的实例保存;
    • 第 33 行:会话被存储。需要进行类型转换,因为方法 [mainActivity.getSession()] 返回的是类型 [ISession];
  • 第36行:从会话中检索片段的先前状态。若为首次访问该片段,仅布尔值[previousState.hasBeenVisited]具有意义;
  • 第39-44行:当首次访问该片段时执行的代码。在此情况下,其先前状态不具有意义;
  • 第44-50行:当非首次访问该片段时执行的代码;
  • 第 46-47 行:若已调用片段构造函数(fragmentHasToBeInitialized==true),则执行此代码;
  • 第48-49行:如果与片段关联的视图已被重建(viewHasToBeInitialized==true),则执行此代码;
  • 第51-52行:根据当前操作(SUBMIT、NAVIGATION、RESTORE)执行代码;
  • 第54-55行:代码仍在执行中;

更新的五个步骤如下:

步骤 1


  // 片段菜单
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // 片段的生命周期
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // 片段状态
  private CoreState previousState;
  // 映射器 jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // 片段生命周期
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // 获取片段的先前状态(首次时,仅布尔值 hasBeenVisited 具有实际意义)
    previousState = session.getCoreState(getNumView());
    // 分多个步骤更新子片段
    // 步骤 1 - 是否为首次访问?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
      // 片段和视图初始化
      initFragment(null);
      initView(null);
      // 清空 previousState 以供后续使用
      previousState = null;
    } else {
      // 这不是首次访问
...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • 第19行:从会话中检索片段的先前状态;
  • 第22-31行:如果片段从未被访问过,则执行此代码;
  • 第27行:要求子类初始化片段。第35行中方法[initFragment]的参数即为片段的先前状态。此处传入null,以告知子片段这是首次访问;
  • 第28行:要求子类初始化与片段关联的视图。第37行中方法[initView]的参数是片段的上一状态。此处传入null,以告知子片段这是首次访问;
  • 第30行:将前一状态设置null,以备后续步骤使用;

步骤 2 和 3


// 片段菜单
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // 片段的生命周期
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // 片段状态
  private CoreState previousState;
  // 映射器 jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // 片段的生命周期
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // 获取片段的先前状态(首次执行时,仅布尔值 hasBeenVisited 具有实际意义)
    previousState = session.getCoreState(getNumView());
    // 分多步更新子片段
    // 步骤 1 - 是否为首次访问?
    if (!previousState.getHasBeenVisited()) {
...
    } else {
      // 不是首次访问
      // 步骤 2:是否需要初始化子片段?
      if (fragmentHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation fragment");
        }
        // 子片段
        initFragment(previousState);
      }
      // 步骤 3:视图是否需要初始化?
      if (viewHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation vue");
        }
        // 子片段
        initView(previousState);
      }
    }

...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • 第24-42行:当这不是片段的首次访问时执行;
  • 第27-33行:如果片段刚刚被重建,则通过调用子类的[initFragment]方法(第32、46行)对其进行重置。向该方法传递片段的先前状态;
  • 第35-51行:如果与片段关联的视图需要初始化或重置,则要求子片段执行此操作(第40、48行)。同样,向其传递片段的最后已知状态;

步骤 4


// 片段菜单
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // 片段的生命周期
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // 片段状态
  private CoreState previousState;
  // 映射器 jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // 片段的生命周期
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // 获取片段的先前状态(首次时,仅布尔值 hasBeenVisited 具有实际意义)
    previousState = session.getCoreState(getNumView());
    // 分多个步骤更新子片段
 ...

    // 步骤 4:提交、导航、恢复?
    // 日志
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
        Log.d(className, String.format("état précédent=%s", jsonMapper.writeValueAsString(previousState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // 操作进行中
    ISession.Action action = session.getAction();
    switch (action) {
      case SUBMIT:
        if (isDebugEnabled) {
          Log.d(className, "updateOnSubmit");
        }
        // 子片段
        updateOnSubmit(previousState);
        break;
      case NAVIGATION:
        if (isDebugEnabled) {
          Log.d(className, "updateForNavigation");
        }
        if (previousState != null) {
          // 菜单恢复
          setMenuOptionsStates(previousState.getMenuOptionsState());
          // 子片段
          updateOnRestore(previousState);
        } else {
          // 这是首次访问 - 无需操作
        }
        break;
      case RESTORE:
        // 恢复
        if (isDebugEnabled) {
          Log.d(className, "updateOnRestore");
        }
        // 菜单恢复(previousState 不能为空)
        setMenuOptionsStates(previousState.getMenuOptionsState());
        // 子片段
        updateOnRestore(previousState);
        break;
    }
....
  protected abstract void updateOnSubmit(CoreState previousState);

protected abstract void updateOnRestore(CoreState previousState);
  • 第34-66行:处理当前操作,该操作可能是以下三种之一:
    • RESTORE:正在设备旋转后恢复片段;
    • NAVIGATION:返回该片段,试图将其恢复到上次使用时的状态;
    • SUBMIT:其他所有情况;
  • 第34行:获取当前操作;
  • 第36-42行:对于类型为SUBMIT的操作,调用子片段的[updateOnSubmit]方法(第41、68行),并向其传递片段的最后已知状态;
  • 第43-55行:针对类型为NAVIGATION的操作;
  • 第47-54行:我们希望将片段恢复到其已知的最后状态。NAVIGATION操作可能与首次访问相关联。例如在标签页应用程序中:如果我从标签页1切换到标签页4:
    • 如果这是首次访问,则需初始化标签页4的片段;
    • 若非首次访问,则将标签页 4 的片段恢复至先前状态;
  • 第52-54行:如果是首次访问,则不执行任何操作。该初始化工作将由子方法 [initView(CoreState previousState)] 负责。首次访问由条件 [previousState==null] 标识;
  • 第49行:如果这不是该片段的首次访问,则恢复其菜单;
  • 第51行:通过调用第70行的方法,要求子类进行更新。向其传递片段的先前状态,以便其执行更新操作;
  • 第56-66行:在片段恢复操作的情况下,处理方式与非首次访问时的导航情况相同;

步骤 5


// 片段菜单
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // 片段生命周期
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // 片段状态
  private CoreState previousState;
  // 映射器 jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // 片段生命周期
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // 步骤 5:终端更新 ----------------------
    // 视图已切换
    session.setPreviousView(getNumView());
    // 无进行中的操作
    session.setAction(ISession.Action.NONE);
    // 离开该片段时,必须将其保存
    saveFragmentDone = false;
    // 只要片段尚未重建,就不需要初始化
    fragmentHasToBeInitialized = false;
    // 只要视图尚未重建,就不需要初始化
    viewHasToBeInitialized = false;
    // 标签页选择功能恢复正常
    session.setNavigationOnTabSelectionNeeded(true);

    // 通知片段视图已准备就绪
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • 第18-30行:到达此处时,片段已初始化并准备显示。此时将片段生命周期管理中使用的所有标志重置为初始状态;
  • 第20行:视图已切换:将此信息记录在会话中;
  • 第22行:当前没有正在执行的操作;
  • 第24行:当即将退出当前显示的片段时,需在退出时对其进行保存;
  • 第26行:片段不再需要重建。当片段构造函数再次执行时,该标志将重置为vrai
  • 第28行:与片段关联的视图不再需要初始化。当再次执行方法[onActivityCreated]时,该标志将重置为vrai
  • 第 30 行:该片段可能显示在标签页应用程序中。在此情况下,当用户点击其中一个标签页时,应进行片段切换;
  • 第36行:通知子类片段已准备就绪。子类可在方法[notifyEndOfUpdates]中执行必须进行的更新操作,或发起异步操作以获取新数据等;

2.7.4. 片段示例

  

我们在项目 [client-android-skel] 中添加了一个片段示例,旨在向读者展示基于该项目构建的应用程序中典型片段的结构。

[DummyFragment] 类的定义如下:


package client.android.fragments.behavior;

import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;

public class DummyFragment extends AbstractFragment {

  // 从父类继承的字段 -------------------------------------------------------

  // 调试模式
  //-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // 类名
  //-- 受保护的字符串 className;
  // 异步任务
  //-- 受保护的 int numberOfRunningTasks;
  // 活动
  //-- protected IMainActivity mainActivity;
  //-- 受保护的活动 activity;
  // 会话
  //-- 受保护的 Session 会话;

  // 从父类继承的方法 -------------------------------------------------------

  // 显示菜单选项
  //-- protected void setAllMenuOptionsStates(boolean isVisible) {
  //-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
  // 管理等待异步任务序列结束
  //-- protected void beginRunningTasks(int numberOfRunningTasks) {
  //-- protected void cancelWaitingTasks() {
  // 使用 RxAndroid 执行异步任务
  //-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
  // 任务取消
  //-- protected void cancelRunningTasks() {
  // 显示异常警报
  //-- protected void showAlert(Throwable th) {
  // 显示消息列表
  //-- protected void showAlert(List<String> messages) {

  // 父类强制的方法 -------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    // 必须保存片段
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // 如果若无内容需保存,则执行 [return new CoreState();] 并删除类 [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // 需将片段编号返回至该业务活动管理的片段表中(参见 MainActivity)
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // 该片段在此步骤或前一步骤中已可见并完成构建
    // 这发生在应用程序启动时以及每次 Android 设备旋转时
    // 之后必然会执行 [initView]
    // 必须初始化已重建片段的字段
    // previousState 是该片段的最新保存记录——若为首次访问该片段,则值为 null
  }

  @Override
  protected void initView(CoreState previousState) {
    // 片段在此步骤或之前的步骤中变得可见,且关联的视图已重建
    // 每次执行 [initFragment] 时,以及片段离开当前显示片段的邻接区域时,都会发生此情况
    // 必须初始化已重建视图的组件
    // previousState 是片段的最新快照——若为首次访问该片段,则值为 null

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // 若执行了这些方法,则在 [initFragment, initView] 之后执行
    // 在执行完 SUBMIT 类型的操作后,视图将被显示
    // 通常需要从会话中初始化片段及其关联视图
    // previousState 是片段的最新保存记录——若为首次访问该片段,则值为 null
    // 如果无法通过 SUBMIT 操作到达该片段,则无需采取任何操作
    // 如果可以通过 SUBMIT 操作从不同的片段到达该片段,则可通过 [session.getPreviousView] 获取上一视图
    // 如果可以通过多个操作 SUBMIT 从同一个片段到达该片段,那么就需要在会话中设置一个标记,以区分从该片段出发的不同类型的 SUBMIT
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // 在 [initFragment, initView] 之后执行,如果这些方法被调用
    // 视图将在执行 RESTORE 或 NAVIGATION 类型的操作后显示
    // previousState 是片段的最新保存版本——绝不会为 null
    // 需将视图恢复至先前状态

  }

  @Override
  protected void notifyEndOfUpdates() {
    // 在方法 [updateOnSubmit, updateOnRestore] 之后执行
    // 到达此处时,视图已构建并初始化
    // 此处通常无需执行任何操作,但也可在此处提取那些无论通过何种方式进入该视图都应执行的操作
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // 当片段发起的异步任务完成或被取消时调用
    // 可通过参数 runningTasksHaveBeenCanceled 区分这两种情况
    // 通常需要将视图恢复到与等待异步任务响应时不同的状态

  }
}

类 [DummyFragment] 可能没有状态。此处添加了一个状态,以提醒其预期用途:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class DummyFragmentState extends CoreState {
  // 片段状态 [DummyFragment]
  // 仅将可序列化的字段放入 jSON
  // 在其他字段上添加 @JsonIgnore 注解,但不清楚它们可能有什么用
  // 切勿遗漏 getter/setter 方法——它们用于序列化/反序列化
}

为了说明 [client-android-skel] 项目的用法,我们将先使用一些简单的示例,然后过渡到更全面的案例研究。

2.8. 演示练习

我们将从重构已编写的示例开始。

2.8.1. 示例-17B

我们重新审视第 1.18 节中研究的示例 17。这是一个仅包含一个片段、没有异步任务且没有标签页的应用程序。我们将对其进行分析,以观察设备旋转时的表现。我们输入以下内容:

Image

随后在 [1] 状态下,我们将设备旋转两次。此时的新视图如下:

Image

若对比这两张视图,除列表 [2] 现已清空外,其余内容均得以保留。

此外,若点击按钮 [Valider],会弹出一个显示表单中已输入内容的对话框。此时若旋转设备,该对话框将消失。

因此,在旋转设备时,我们需要重新生成:

  • 下拉列表及其所选项;
  • 如果旋转时对话框正在显示,则需重新生成该对话框;

2.8.1.1. 项目 [Exemple-17B]

我们将项目 [client-android-skel] 复制到 examples/Exemple-17B 目录下。然后加载新项目 [1]:

  • 在 [2-3] 中,位于 [behavior] 文件夹内,粘贴来自项目 [Exemple-17] 的片段 [Vue1Fragment];
  • 在 [4-5] 中,将 [Exemple-17B] 的 [layout] 文件夹中的 [vue1.xml] 视图粘贴到 [Exemple-17] 中。 这是与该片段关联的视图;
  • 在 [6] 中,将 [Exemple-17B] 的文件夹 [values] 替换为 [Exemple-17] 的文件夹 [values];

将视图 [vue1.xml] 的上边距修改为 80 dp:


    <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="80dp"
      android:text="@string/titre_vue1"
android:textSize="30sp"/>

此时,可以尝试首次编译以查看错误。最初报告的错误源于 imports 中包的位置发生了变化。修正这些错误(Ctrl-Shift-O)。 其他错误则源于视图 [Vue1Fragment] 未实现其父类 [AbstractParent] 规定的所有方法:

Image

生成缺失的方法(Alt-Enter)。

另一个报告的编译错误如下:

Image

在模块的 [build.gradle] 文件中修正此问题(如下所示的第 20 行):

 

此时,可以重新编译以查看剩余的错误。唯一报告的错误出现在方法 [Vue1Fragment.updateFragment] 上:

 

需删除第 135 行中的注释 [@Override]。现在已无错误。我们将以此为基础修改项目。

2.8.1.2. 片段 [Vue1Fragment] 的状态

片段 [Vue1Fragment] 需要在设备旋转时保存信息,以便能够完全恢复。为此,我们创建了一个类 [Vue1FragmentState]:

  

目前,该类为空:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {
  
}

2.8.1.3. 项目自定义

  

在文件夹 [custom] 中包含开发人员可自定义的架构组件。

[IMainActivity] 接口的常量如下:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // 访问会话
  ISession getSession();

  // 视图切换
  void navigateToView(int position, ISession.Action action);

  // 等待管理
  void beginWaiting();

  void cancelWaiting();

  // 应用程序常量 -------------------------------------

  // 调试模式
  boolean IS_DEBUG_ENABLED = true;

  // 服务器响应的最大等待时间
  int TIMEOUT = 1000;

  // 执行客户端请求前的等待时间
  int DELAY = 0;

  // 基本身份验证
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // 片段邻接性
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // 标签栏
  boolean ARE_TABS_NEEDED = false;

  // 加载图片
  boolean IS_WAITING_ICON_NEEDED = false;

  // 应用程序的片段数量
  int FRAGMENTS_COUNT = 1;

}
  • 第 24-31 行:应用程序在此处未使用其 [DAO] 层。这些常量将不会被使用;
  • 第 34 行:片段邻接度为 1,这是默认值。由于应用程序只有一个片段(第 43 行),因此该值无关紧要;
  • 第 39-40 行:由于没有涉及 [DAO] 层的操作,因此无需保留待处理图像;
  • 第 37 行:这不是一个标签页应用程序;
  • 第 43 行:仅有一个片段;

类 [Session] 如下所示:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // 无法序列化为 jSON 的元素必须带有 @JsonIgnore 注解

}

该类为空。实际上,由于仅有一个片段,因此无需通过会话实现片段间的通信。

最后,类 [CoreState] 如下:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // 片段是否已被访问
  protected boolean hasBeenVisited = false;
  // 片段菜单(如有)的状态
  protected MenuItemState[] menuOptionsState;

  // 获取器和设置器
...
}
  • 第 11-13 行:我们需要列出所有从 [CoreState] 派生且用于存储不同片段状态的类。此处仅有一个(第 12 行);

2.8.1.4. [MainActivity] 活动

[MainActivity] 活动当前内容如下:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // [DAO] 层
  @Bean(Dao.class)
  protected IDao dao;
  // 会话
  private Session session;

  // 父类方法 -----------------------
  @Override
  protected void onCreateActivity() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // 会话
    this.session = (Session) super.session;
    // 待办事项:继续父类已开始的初始化工作
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // 待办事项:在此处定义片段
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // 待办事项:在此处定义片段标题
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // 待办事项:标签页导航——定义选中标签页 [position] 时显示的视图
  }

  @Override
  protected int getFirstView() {
    // 待办事项:定义要显示的第一个视图(片段)的编号
    return 0;
  }
}

注释 [//todo] 说明了开发人员需要执行的操作。类 [MainActivity] 的演变如下:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // 层 [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // 会话
  private Session session;

  // 父类方法 -----------------------
  @Override
  protected void onCreateActivity() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // 会话
    this.session = (Session) super.session;
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new Vue1Fragment_()};
  }

  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {

  }

  @Override
  protected int getFirstView() {
    return 0;
  }
}

仅需修改第 41-44 行中的方法。该方法需返回应用程序的片段数组。第 43 行中,请务必在片段名称后添加下划线

2.8.1.5. 片段 [FragmentState] 的状态

根据对项目 [Exemple-17] 进行的轮转测试,决定存储片段的以下元素:

  • 下拉列表中的值列表;
  • 该列表中选中项的位置;
  • 若在旋转时存在对话框,则保存其显示的提示信息;

[Vue1FragmentState]类将如下所示:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

import java.util.List;

public class Vue1FragmentState extends CoreState {

  // 下拉列表中的值
  private List<String> list;
  // 下拉列表中选中的项
  private int listSelectedPosition;
  // 对话框中显示的消息
  private String message;

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

2.8.1.6. 片段 [AbstractFragment]

目前,片段的生命周期由两个方法(第 6 行和第 32 行)管理:


// 下拉列表
  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");
  }
...
  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);
  }

这两个方法的代码将按以下方式迁移到 [AbstractFragment] 类定义的方法中:


// 片段生命周期管理 ---------------------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    Vue1FragmentState state = new Vue1FragmentState();
    state.setList(list);
    state.setListSelectedPosition(dropDownList.getSelectedItemPosition());
    state.setMessage(message);
    return state;
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // 首次访问?
    if (previousState == null) {
      // 创建下拉列表的选项
      list = new ArrayList<>();
      list.add("list 1");
      list.add("list 2");
      list.add("list 3");
    } else {
      // 还原下拉列表的值
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      list = state.getList();
      // 以及对话框中的消息
      message = state.getMessage();
    }
    // 下拉列表适配器初始化
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
  }

  @Override
  protected void initView(CoreState previousState) {
    // 日历
    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));
      }
    });
    // 下拉列表适配器初始化
    dropDownList.setAdapter(dataAdapter);
    // 首次访问?
    if (previousState == null) {
      // 勾选第一个按钮
      radioButton1.setChecked(true);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // 滑动条值
    seekBarValue.setText(String.valueOf(seekBar.getProgress()));
    // 下拉列表中选中的项目
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    dropDownList.setSelection(state.getListSelectedPosition());
    // 对话框是否可见?
    if (message != null) {
      // 显示
      showMessage();
    }
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

}
  • 第2-9行:方法[saveFragment]需将待存储的片段元素放入[CoreState]的派生类中,并返回该类的实例;
  • 第11-14行:方法[getNumView]应返回片段编号。此处仅有一个片段,其编号为0;
  • 第16-34行:方法[initFragment]需初始化片段字段。该方法接收片段的先前状态。若[previousState]等于null,则表示这是首次访问;
  • 第19-25行:首次访问时,创建下拉列表的值;
  • 第26-30行:若非首次访问,则根据上一次状态恢复片段中的[list, message]字段;
  • 第33-34行:初始化片段的[dataAdapter]字段。这是下拉列表的数据源;
  • 第37-62行:方法[initView]用于初始化视觉界面的组件。它接收前一状态[previousState]作为参数。若为[previousState==null],则表示首次访问;
  • 此处包含此前 [@AfterViews] 方法中的内容;
  • 第57-61行:在首次访问时,确保第一个单选按钮被选中;
  • 第64-67行:当当前操作为[SUBMIT]时,将执行方法[updateOnSubmit]。此处不存在跨片段导航,因此没有当前操作;
  • 第 69-81 行:当当前操作为 [NAVIGATION] 或 [RESTORE] 时,将执行方法 [updateOnRestore]。 此处没有片段间导航,因此无法执行 [NAVIGATION] 操作;
  • 第 72 行:重新计算(而非恢复)TextView 的值 seekBarValue。因为在旋转过程中,该值有时会丢失;
  • 第74-75行:将列表定位到旋转前被选中的元素。否则,列表将定位到其第一个元素;
  • 第76-80行:如果前一状态的消息不是null,则重新显示对话框。我们将稍后讨论方法[showMessage](第79行);
  • 第83-86行:方法[notifyEndOfUpdates]是父类在放手让子片段独立运行前调用的最后一个方法。此处无需执行任何操作;
  • 第88-91行:方法[notifyEndOfTasks]用于标记片段启动的异步任务结束。此处没有此类任务;

对话框的恢复操作如下:


  // 对话框中的消息
  private String message;
...
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // 要显示的消息列表
    List<String> messages = new ArrayList<>();
    ...
    // 显示
    doAfficher(messages);
  }

  private void doAfficher(final List<String> messages) {
    // 构建要显示的文本
    StringBuilder texte = new StringBuilder();
    for (String message : messages) {
      texte.append(String.format("%s\n", message));
    }
    // 存储消息
    message = texte.toString();
    // 显示消息
    showMessage();
  }

  private void showMessage() {
    // 显示
    new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        // 重置消息
        message = null;
      }
    }).show();
}

当用户提交表单时,方法 [doValider](第 5 行)会构建一个消息列表,随后将其显示(第 10 行)在对话框中。

  • 第14-20行:消息列表被拼接成一条消息,并存储在第2行;
  • 第25-33行:对话框显示的就是这条消息,方法[updateOnRestore]同样会显示这条消息;
  • 第27行:方法[setNeutralButton]的第二个参数是用户点击对话框中的按钮[Fermer]时执行的方法;
  • 第 31 行:在关闭对话框时,将消息重置为 null,以指示对话框已不存在;

2.8.1.7. Tests

请读者测试此项目,并验证在经历一次或多次连续旋转后,该片段是否仍被保留。

2.8.2. 示例-23:天气客户端

某些网站提供以 jSON 字符串形式呈现的气象信息。以下是一个示例:

Image

URL 的格式为: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID}其中:

  • city:所需天气的城市,此处为昂热;
  • country:该城市的国家,此处为法国(fr);
  • APPID:在 [https://home.openweathermap.org/users/sign_up] 网站注册后获得的密钥;

2.8.2.1. 该项目

  

该项目基于项目 [client-android-skel] 构建而成。它具有以下特点:

  • 它仅包含一个无需保存状态的片段;
  • 它执行异步请求;

2.8.2.2. 项目定制

  

通过 [IMainActivity] 接口可指定项目的某些特性:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // 访问会话
  ISession getSession();

  // 切换视图
  void navigateToView(int position, ISession.Action action);

  // 等待管理
  void beginWaiting();

  void cancelWaiting();

  // 应用程序常量 -------------------------------------

  // 调试模式
  boolean IS_DEBUG_ENABLED = true;

  // 服务器响应的最大等待时间
  int TIMEOUT = 1000;

  // 执行客户端请求前的等待时间
  int DELAY = 5000;

  // 基本身份验证
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // 片段邻接性
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // 标签栏
  boolean ARE_TABS_NEEDED = false;

  // 加载图片
  boolean IS_WAITING_ICON_NEEDED = true;

  // 应用程序片段数量
  int FRAGMENTS_COUNT = 1;

}
  • 第 25、28、31、40 行:[DAO] 层的特性。第 31 行,无需基本身份验证;
  • 第34行:片段的相邻性。此处该常量无关紧要,因为只有一个片段;
  • 第37行:这不是一个带标签页的应用程序;
  • 第 43 行:仅有一个片段;

用于保存片段状态的类 [CoreState] 如下所示:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// 待办事项:在此添加 [CoreState] 的子类
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // 片段是否已被访问
  protected boolean hasBeenVisited = false;
  // 片段菜单的状态(如有)
  protected MenuItemState[] menuOptionsState;

  // 获取器和设置器
...
}
  • 第10-13行:无需声明,因为该应用中仅有一个片段,且不保存其状态;

类 [Session] 如下所示:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // 无法序列化为 jSON 的元素必须带有 @JsonIgnore 注解
}

该类为空,因为该应用程序中不存在片段间通信。

2.8.2.3. [DAO] 层

  

在 [DAO] 层中,有三个类需要进行自定义:

  • 接口 IDao
  • Dao 实现;
  • 与Web服务器交互的接口WebClient / jSON;

接口 [WebClient] 将如下所示:


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // 气象服务
  @Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
  String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
  • 第18-19行:气象服务的URL。 需注意,该接口与客户端的根接口 URL(RestClientRootUrl,第 12 行)相关。此处,该根接口 URL 将变为 [http://api.openweathermap.org/];

[IDao]接口将如下所示:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // Web 服务 URL
  void setUrlServiceWebJson(String url);

  // 用户
  void setUser(String user, String mdp);

  // 客户端超时
  void setTimeout(int timeout);

  // 基本身份验证
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

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

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

  //  气象服务
  Observable<String> getWeatherForecast(String city, String country, String APPID);
}
  • 需注意,第6至22行的方法默认存在于项目[client-android-skel]的接口IDao中;
  • 第25行:方法[getWeatherForecast]用于获取国家[country]下城市[city]的天气信息字符串jSON。 第三个参数是从网站 [https://home.openweathermap.org/users/sign_up] 获取的密钥;

接口 [IDao] 由以下类 [Dao] 实现:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // Web 服务客户端
  @RestService
  protected WebClient webClient;
  // 安全性
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  // RestTemplate 的工厂
  private SimpleClientHttpRequestFactory factory;
  // 超时
  private int timeout;

  @AfterInject
  public void afterInject() {
    // 日志
    Log.d(className, "afterInject");
    // 构建 restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // 设置转换器 jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // 设置 Web 客户端的 restTemplate
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // 设置 Web 服务的 URL
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // 将用户记录到拦截器中
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // 内存
    this.timeout = timeout;
    // 配置工厂
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // 身份验证拦截器?
    if (isBasicAuthentificationNeeded) {
      // 添加身份验证拦截器
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }


  // 私有方法 -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // 气象服务 ---------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
    // 日志
    if (isDebugEnabled) {
      Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
    }
    // 结果
    return getResponse(new IRequest<String>() {
      @Override
      public String getResponse() {
        return webClient.getWeatherForecast(city, country, APPID);
      }
    });
  }
}
  • 需要提醒的是,第17至90行默认存在于项目[client-android-skel]的类[Dao]中。只需添加针对该应用程序的、用于实现接口[IDao]的方法(第92行);
  • 第93-105行:实现方法[getWeatherForecast]。该方法非常简单,仅需6行代码(第100-105行);
  • 第 100 行:方法 [getResponse] 是父类 [AbstractDao] 的一个方法。 它期望接收一个类型为 [IRequest<T>] 的参数,其中 T 是服务器预期返回的响应类型,此处为 String,因为我们期望接收一个 jSON 类型的字符串。 [IRequest<T>] 的类型 T 必须是方法 [Observable<T> getWeatherForecast] 的类型 T;
  • 接口 [IRequest<T>] 仅有一个方法:getResponse。该方法的作用是提供方法 [Observable<T> getWeatherForecast] 必须返回的类型 T 的响应;
  • 第 103 行:由接口 [WebClient] 提供此响应。向其传递第 94 行接收的三个参数。因此,这些参数必须带有 final 属性;

2.8.2.4. [MainActivity] 活动

  

[MainActivity] 活动如下:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.MeteoFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // [DAO] 层
  @Bean(Dao.class)
  protected IDao dao;

  // 父类方法 -----------------------
  @Override
  protected void onCreateActivity() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new MeteoFragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
  }

  @Override
  protected int getFirstView() {
    return 0;
  }

  // 接口 IDao ---------------------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(String city, String country, String APPID) {
    return dao.getWeatherForecast(city, country, APPID);
  }
}
  • 需注意,第15-55行在项目[client-android-skel]中默认存在。只需对其进行自定义;
  • 第37-40行:片段表。此处仅有一个;
  • 第43-46行:无需片段标题;
  • 第48-50行:此处无需标签页;
  • 第52-55行:要显示的第一个视图是第0号视图,即[MeteoFragment]视图;
  • 第58-61行:实现[IDao]接口。此处无需其他操作,只需将工作委托给第21行的[DAO]层;

2.8.2.5. 代码片段[MeteoFragment]

  

片段 [MeteoFragment] 向气象 Web 服务 / jSON 发起请求。其框架如下:


package client.android.fragments;

import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.AbstractFragment;
import client.android.architecture.MenuItemState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action0;
import rx.functions.Action1;

@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class FirstFragment extends AbstractFragment {
...
}
  • 第 14 行:视图 [res / layout / meteo_fragment.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="Construisez votre interface visuelle"
    android:id="@+id/textView" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true" android:layout_marginLeft="64dp" android:layout_marginStart="64dp"
    android:layout_marginTop="120dp"/>
</RelativeLayout>

该视图仅显示第 10 行的文本;

  • 第15行:[res / menu / menu_meteo.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/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionMeteo"
        android:title="@string/actionMeteo"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
      <item
        android:id="@+id/actionTerminer"
        android:title="@string/actionTerminer"/>
    </menu>
  </item>
</menu>
  • 第10-12行:此菜单选项用于查询某城市的天气;
  • 第14-15行:此菜单选项用于取消正在进行的查询;
  • 第16-18行:此菜单选项用于退出应用程序;

该代码片段的完整代码如下:


package client.android.fragments.behavior;

import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action1;

@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class MeteoFragment extends AbstractFragment {

  // 本地数据
  private int nbReponsesRecues;

  // 事件管理 ---------------------------------------------------------------------------------------
  // 需要获取天气的城市
  final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

  @OptionsItem(R.id.actionMeteo)
  protected void doMeteo() {
    // 所在国家
    String country = "fr";
    // 通过创建账户获取 API 标识符 [https://home.openweathermap.org/users/sign_up]
    String APPID = "xyz";
    // Web 服务的 URL / jSON
    mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
    // 开始等待异步任务 [paysDeLoire.length]
    beginWaiting(paysDeLoire.length);
    // 已接收的响应数量
    nbReponsesRecues = 0;
    // 并行执行异步调用
    for (String city : paysDeLoire) {
      // 天气
      executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
        @Override
        public void call(String response) {
          // 响应处理
          consumeResponse(response);
          // 一个正向响应
          nbReponsesRecues++;
        }
      });
    }
  }

  // 服务器响应处理
  private void consumeResponse(String response) {
    // 日志
    Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
  }

  // 开始等待
  protected void beginWaiting(int numberOfRunningTasks) {
    // 日志
    if (isDebugEnabled) {
      Log.d(className, "beginWaiting");
    }
    // 父级
    beginRunningTasks(numberOfRunningTasks);
    // 显示选项 [Annuler]
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{
      new MenuItemState(R.id.menuActions, true),
      new MenuItemState(R.id.actionAnnuler, true)});

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // 菜单
    initMenu();
    // 显示结果
    String message;
    switch (nbReponsesRecues) {
      case 0:
        message = "Aucune réponse n'a été reçue";
        break;
      case 1:
        message = "Une réponse a été reçue. Consultez vos logs...";
        break;
      default:
        message = String.format("%s réponses ont été reçues. Consultez vos logs...", nbReponsesRecues);
        break;
    }
    Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
  }

  // 私有方法 -----------------------------------
  private void initMenu() {
    if (isDebugEnabled) {
      Log.d(className, "initMenu");
    }
    // 菜单
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  // 生命周期管理 ---------------------------------------------------------------------------------------
...
}
  • 第25-50行:处理对菜单选项[Météo]的点击;
  • 第 32 行:构建 Web 服务 URL / 气象服务 jSON。随后通过活动将其传递至 [DAO] 层;
  • 第34行:开始等待。传入将要启动的任务数量,以便父类能通知我们任务结束。此处有五个任务,因为我们将查询第23行中五个城市的天气;
  • 第16行:统计已接收的响应数量以便显示;
  • 第38-50行:循环遍历需要查询天气的城市;
  • 第40行:将并行发起5个HTTP请求;
  • 第40行:要求父类[AbstractParent]调用Web服务/jSON;
  • 第40-48行:方法 [executeInBackground] 需要两个参数:
    • 第 40 行:待观察和执行的进程由方法 [mainActivity.getWeatherForecast] 提供;
    • 第 40-48 行:当收到异步服务响应时需执行的 [Action1] 实例。[Action1<T>] 的类型 T 必须是 [getWeatherForecast] 方法返回结果的类型 T;
  • 第 44 行:已收到响应。将其传递给第 53 行的 [consumeResponse] 方法;
  • 第 46 行:递增已接收响应的计数器;
  • 第53-56行:处理来自气象服务的jSON响应;
  • 第 55 行:仅记录字符串 jSON;
  • 第59-72行:在启动异步任务前执行的代码;
  • 第 65 行:将待执行任务的数量传递给父类 [AbstractParent]。这使得该类能在所有任务完成后通知我们;
  • 第67-70行:为等待状态准备菜单。仅保留[Actions/Annuler]选项,该选项允许用户取消已启动的任务;
  • 第 74-92 行:当父类通知所有已启动任务均已完成时执行的代码;
  • 第 77 行:将菜单恢复为初始状态。方法 [initMenu](第 95-102 行)显示包含所有选项的菜单,但隐藏选项 [Actions/Annuler];
  • 第80-91行:显示已接收的回复数量;

点击菜单选项 [Annuler] 由以下代码处理:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // 取消异步任务
    cancelRunningTasks();
}
  • 第7行:请求父类取消仍在运行的任务;

点击菜单选项 [Terminer] 由以下代码处理:


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminer() {
    // 停止所有操作
    System.exit(0);
}

片段的生命周期管理由以下方法负责:


  // 生命周期管理 ---------------------------------------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {

  }

  @Override
  protected void initView(CoreState previousState) {
    // 首次访问?
    if (previousState == null) {
      initMenu();
    }
  }


  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

}
  • 第 3-6 行:用于在 [CoreState] 的派生类中保存片段的状态。如果片段没有需要保存的状态(如本例),则仅返回 [CoreState] 的实例。 切勿返回 null,否则后续会导致程序崩溃;
  • 第 8-11 行:必须返回视图编号。此处片段 [MeteoFragment] 的编号为 0;
  • 第13-16行:用于在片段构建完成(previousState==null)或重建(previousState!=null)后对其进行初始化。此处无需操作。唯一可能需要初始化的字段如下:

  // 需要查询天气的城市
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

但它会自动初始化;

  • 第18-24行:用于在片段构建(previousState==null)或重建(previousState!=null)后初始化与其关联的视图;
  • 第21-23行:若为首次访问该片段,则初始化其菜单以隐藏选项 [Annuler];
  • 第27-30行:若通过[SUBMIT]类型的操作导航到达该片段,则调用此代码。此处仅有一个片段,因此不存在片段间的导航;
  • 第32-35行:当因设备旋转或其他原因触发保存/恢复循环时调用。此处因未保存状态,故无需执行任何操作;
  • 第 37-40 行:在所有先前更新完成后被调用。此处无需执行任何操作;

2.8.2.6. Tests

现在我们运行该示例:

Image

Image

此时日志如下:


07-23 13:24:30.899 2642-2642/client.android D/MainActivity_: constructeur
07-23 13:24:30.945 2642-2642/client.android D/AbstractDao: constructeur, thread=main
07-23 13:24:32.861 2642-2642/client.android D/client.android.dao.service.Dao_: afterInject
07-23 13:24:32.950 2642-2642/client.android D/MainActivity_: onCreate
07-23 13:24:32.951 2642-2642/client.android D/client.android.dao.service.Dao_: setTimeout thread=main, timeout=1000
07-23 13:24:32.952 2642-2642/client.android D/client.android.dao.service.Dao_: setBasicAuthentification thread=main, isBasicAuthentificationNeeded=false
07-23 13:24:33.041 2642-2642/client.android D/MainActivity_: adding loadingPanel
07-23 13:24:33.043 2642-2642/client.android D/MeteoFragment_: constructeur
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: navigation vers vue 0 sur action NONE
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: onCreateActivity
07-23 13:24:33.080 2642-2642/client.android D/MainActivity_: onResume
07-23 13:24:33.325 2642-2642/client.android D/MeteoFragment_: onActivityCreated
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: onCreateOptionsMenu
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: getMenuOptionsStates(Menu)
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: Nombre d'options de menu=4
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initFragment initView updateForFirstVisit
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initMenu
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: session={"action":"NONE","coreStates":[{"@type":"CoreState","hasBeenVisited":false,"menuOptionsState":null}],"previousTab":0,"previousView":0}
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: état précédent=null
07-23 13:24:33.558 2642-2642/client.android D/MeteoFragment_: notifyEndOfUpdates
07-23 13:24:39.766 2642-2642/client.android D/MeteoFragment_: beginWaiting
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=angers, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=le mans, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=nantes, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=laval, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=la roche sur yon, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:45.035 2642-2961/client.android D/client.android.dao.service.Dao_: response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200} sur thread [RxIoScheduler-4]
07-23 13:24:45.035 2642-2963/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-6]
07-23 13:24:45.035 2642-2959/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-2]
07-23 13:24:45.035 2642-2962/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-5]
07-23 13:24:45.036 2642-2960/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-3]
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: initMenu
  • 第 32-36 行:在 I/O 线程上获取到响应 jSON
  • 第37-41行:该片段在UI线程上获取了5个响应;

现在,我们使用一个错误的标识符 API 进行请求:


    String APIID = "";

Image

此时日志如下:


07-23 13:34:43.853 11240-11240/client.android D/MeteoFragment_: beginWaiting
...
07-23 13:34:49.121 11240-11464/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.121 11240-11466/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11468/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11467/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception reçue
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Annulation des tâches lancées
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: initMenu
07-23 13:34:49.167 11240-11465/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
  • 第 3-6 行、第 10 行:5 次对 HTTP 的调用共引发了 5 个异常;
  • 第7行:片段[MeteoFragment]接收到第一个异常。随后它将取消所有任务;

现在为 [IMainActivity.DELAY] 设置 5 秒的等待时间并取消操作。此时日志如下:


07-21 13:16:20.329 20390-20390/client.android D/MeteoFragment_: beginWaiting
...
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation demandée
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation des tâches lancées
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: initMenu
07-21 13:25:02.948 29965-30197/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
  • 第3行:取消请求;
  • 第 4 行:由于已发生取消,因此取消等待;
  • 第6-10行:任务的取消会在五个任务的每个线程上引发异常。异常类型取决于具体应用。 此处的异常为 [java.lang.InterruptedException],因为任务在执行 [Thread.sleep(delay)] 指令时被中断,该指令会使其人为地等待 [delay] 毫秒;

2.8.3. 示例-16B

在此我们重构第 1.17 节中的示例 16。该示例展示了一个向随机数服务器进行异步调用的代码片段。让我们看看在设备旋转时它的表现如何:

Image

  • 在 [1] 中,设备旋转了两圈;

Image

可以看到所有错误消息都丢失了。我们将尝试改进这一点。

2.8.3.1. 示例-16B项目

我们将项目 [client-android-skel] 复制到项目 [exemples/Exemple-16B] 中,然后加载新项目:

  

从初始项目 [Exemple-16] 中,我们将以下内容复制到 [Exemple-16B] 中:

  • 文件 [res/layout/vue1.xml],文件夹 [res/values]:
  

我们将视图 [vue1.xml] 的上边距修改为 80 dp:


  <TextView
    android:id="@+id/txt_Titre2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="80dp"
    android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
  • 片段 [Vue1Fragment]:
  
  • 类 [dao / service / Response]:
  

此时,我们可以尝试进行首次编译:

  • 第一类错误涉及 imports。在向 [Exemple-16B] 迁移的过程中,某些类已更改了所属包。我们首先修正此类错误;
  • 第二类错误出现在类 [Vue1Fragment] 上,因为它未实现父类 [AbstractParent] 强制要求的方法。我们自动生成这些方法;

尝试第二次编译:

  • 所有剩余错误现均集中在类 [Vue1Fragment] 上,该类将经历最多的修改;

2.8.3.2. 为片段 [Vue1Fragment] 创建状态

我们已了解到,在旋转操作中,某些片段信息必须被保存,以便在旋转后恢复片段至原始状态。因此,我们先创建一个暂为空的 [Vue1FragmentState] 状态:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {

}

2.8.3.3. 项目自定义

  

[IMainActivity] 界面允许指定项目的某些特性:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // 登录
  ISession getSession();

  // 切换视图
  void navigateToView(int position, ISession.Action action);

  // 等待管理
  void beginWaiting();

  void cancelWaiting();

  // 应用程序常量 -------------------------------------

  // 调试模式
  boolean IS_DEBUG_ENABLED = true;

  // 服务器响应的最大等待时间
  int TIMEOUT = 1000;

  // 执行客户端请求前的等待时间
  int DELAY = 5000;

  // 基本身份验证
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // 片段邻接性
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // 标签栏
  boolean ARE_TABS_NEEDED = false;

  // 加载图片
  boolean IS_WAITING_ICON_NEEDED = true;

  // 应用程序片段数量
  int FRAGMENTS_COUNT = 1;

}
  • 第 25、28、31、40 行:图层 [DAO] 的特征。无需基本身份验证;
  • 第 34 行:片段的相邻性。此处该常量无关紧要,因为只有一个片段;
  • 第37行:这不是一个带标签页的应用程序;
  • 第 43 行:仅有一个片段;

用于保存片段状态的类 [CoreState] 将如下所示:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // 片段是否已被访问
  protected boolean hasBeenVisited = false;
  // 片段菜单(如有)的状态
  protected MenuItemState[] menuOptionsState;

  // 获取器和设置器
...
}
  • 第12行:我们声明片段状态类 [Vue1Fragment];

类 [Session] 如下所示:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // 无法序列化为 jSON 的元素必须带有 @JsonIgnore 注解
}

该类为空,因为在此应用中不存在片段间通信。

2.8.3.4. [DAO] 层

  

在 [DAO] 层中,有三个类需要进行自定义:

  • 接口 IDao
  • Dao 实现;
  • 用于与Web服务器通信的接口WebClient / jSON;

类 [Response] 来自项目 [Exemple-16],该项目使用该类:


package client.android.dao.service;

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

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

[WebClient] 接口将如下所示:


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // 1 个位于区间 [a,b] 内的随机数
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);

}
  • 第 18-19 行:随机数服务中的 URL。 需注意,该接口与客户端的根接口 URL(RestClientRootUrl,第12行)相关。在此,该根接口 URL 将变为 [http://localhost:8080];

[IDao]接口如下:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // Web 服务的 URL
  void setUrlServiceWebJson(String url);

  // 用户
  void setUser(String user, String mdp);

  // 客户端超时
  void setTimeout(int timeout);

  // 基本身份验证
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

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

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

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

}
  • 需注意,第6至22行的方法默认存在于项目[client-android-skel]的接口IDao中;
  • 第25行:方法[getAlea]用于获取[a,b]区间内的随机数。 该数值通过类型为 [Response<Integer>] 的响应获取,其中随机数位于该类型的 [body] 字段中;

接口 [IDao] 由以下类 [Dao] 实现:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // Web 服务客户端
  @RestService
  protected WebClient webClient;
  // 安全性
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  // RestTemplate 的工厂
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // 日志
    Log.d(className, "afterInject");
    // 正在构建 restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // 安装 jSON 转换器
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // 设置 Web 客户端的 restTemplate
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // 设置 Web 服务的 URL
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // 在拦截器中注册用户
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // 配置工厂
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // 身份验证拦截器?
    if (isBasicAuthentificationNeeded) {
      // 添加身份验证拦截器
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // 私有方法 -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // 随机数服务
  @Override
  public Observable<Response<Integer>> getAlea(final int a, final int b) {
    // Web 客户端执行
    return getResponse(new IRequest<Response<Integer>>() {
      @Override
      public Response<Integer> getResponse() {
        return webClient.getAlea(a, b);
      }
    });
  }

}
  • 需要提醒的是,第17-85行默认存在于项目[client-android-skel]的类[Dao]中。只需添加接口[IDao]的实现方法即可;
  • 第88-97行:实现[getAlea]方法。该方法非常简单,仅需6行代码(第91-96行);
  • 第91行:方法[getResponse]是父类[AbstractDao]的一个方法。 它期望接收一个类型为 [IRequest<T>] 的参数,其中 T 是预期响应的类型,此处为类型 Response<Integer>。 [IRequest<T>](第 91 行)的类型 T 必须是方法 [Observable<T> getAlea](第 89 行)的类型 T;
  • 接口 [IRequest<T>] 仅有一个方法:getResponse。该方法的作用是提供方法 [Observable<T> getAlea] 应返回的 T 类型响应;
  • 第 94 行:由接口 [WebClient] 提供此响应。向其传递第 89 行接收到的两个参数。因此,这些参数必须具有 final 属性;

2.8.3.5. [MainActivity] 活动

  

[MainActivity] 活动如下:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // [DAO] 层
  @Bean(Dao.class)
  protected IDao dao;

  // 父类方法 -----------------------
  @Override
  protected void onCreateActivity() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // 继续由父类开始的初始化
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // 在此处定义片段
    return new AbstractFragment[]{new Vue1Fragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // 在此处定义片段标题
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // 标签页导航 - 定义要显示的视图
  }

  @Override
  protected int getFirstView() {
    return 0;
  }

  // IDao 界面 ------------------------------------------
  @Override
  public Observable<Response<Integer>> getAlea(int a, int b) {
    return dao.getAlea(a, b);
  }

}
  • 需注意,第15-61行在项目[client-android-skel]中默认存在。只需对其进行自定义;
  • 第40-44行:片段表。此处仅有一个;
  • 第47-51行:无需片段标题;
  • 第53-56行:此处无需标签页;
  • 第58-61行:要显示的第一个视图是第0号视图,即[Vue1Fragment]视图;
  • 第64-67行:实现[IDao]接口。此处无需其他操作,只需将工作委托给第23行的[DAO]层;

2.8.3.6. 片段[Vue1Fragment]的状态

  

[Vue1FragmentState]类将如下所示:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

import java.util.ArrayList;
import java.util.List;

public class Vue1FragmentState extends CoreState {

  // 片段状态 ------------------------
  // 回答列表
  private List<String> reponses = new ArrayList<>();
  // 视图状态 ------------------------
  // 关于请求随机数数量的错误消息
  private boolean txtErrorAleasVisible = false;
  // 关于生成区间 [a,b] 的错误消息
  private boolean txtErrorIntervalleVisible = false;
  // Web 服务 URL 相关错误信息
  private boolean txtMsgErreurUrlServiceWebVisible = false;
  // 等待时间错误消息
  private boolean textViewErreurDelayVisible = false;
  // “执行”按钮的可见状态
  private boolean btnExecuterVisible = true;

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

为了确定片段中需要存储的内容,我们让设备在各种情况下进行旋转,并观察恢复后丢失了哪些内容。最终得出结论,需要存储第10至23行的信息。

2.8.3.7. 片段[Vue1Fragment]

  

目前,视图 [Vue1Fragment] 存在多种错误,这是因为其派生自的父类 [AbstractFragment] 已发生变更。与其逐一描述需要进行的修改,我们不如直接对最终版本进行说明。

该片段的框架如下:


package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.*;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.dao.service.Response;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.Observable;
import rx.functions.Action1;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vide)
public class Vue1Fragment extends AbstractFragment {

...
}
  • 第 26 行 需注意,所有片段都必须包含一个菜单,即使为空。此处即为如此。

2.8.3.7.1. 按钮点击处理 [Exécuter]

@Click(R.id.btn_Executer)
  protected void doExecuter() {
    // 验证输入数据
    if (!isPageValid()) {
      return;
    }
    // 清除先前回答
    reponses.clear();
    dataAdapterReponses.notifyDataSetChanged();
    // 将响应计数器重置为0
    nbReponses = 0;
    infoReponses.setText("Liste des réponses (0)");
    // 初始化活动
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // 准备随机任务
    beginWaiting(1);
    // 请求随机数
    getAleasInBackground(nbAleas, a, b);
  }

  void getAleasInBackground(int nbAleas, int a, int b) {
    // 创建待观察进程
    Observable<Response<Integer>> process = Observable.empty();
    for (int i = 0; i < nbAleas; i++) {
      process = process.mergeWith(mainActivity.getAlea(a, b));
    }
    // 请求随机数
    executeInBackground(process, new Action1<Response<Integer>>() {

      @Override
      public void call(Response<Integer> response) {
        // 处理响应
        consumeAleaResponse(response);
      }
    });
  }

  protected void consumeAleaResponse(Response<Integer> response) {
    // 日志
    if (isDebugEnabled) {
      try {
        Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // 收到 + 的响应
    nbReponses++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
    // 分析响应
    // 错误?
    if (response.getStatus() != 0) {
      // 显示
      showAlert(response.getMessages());
      // 取消
      doAnnuler();
      // 返回用户界面
      return;
    }
    // 将信息添加到响应列表中
    reponses.add(0, String.valueOf(response.getBody()));
    // 刷新答案
    dataAdapterReponses.notifyDataSetChanged();
  }

  // 取消----------
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // 取消异步任务
    cancelRunningTasks();
}

  private void beginWaiting(int nbRunningTasks) {
    // 显示沙漏
    beginRunningTasks(nbRunningTasks);
    // 按钮 [Annuler] 替换按钮 [Exécuter]
    btnExecuter.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
  }
  • 第4-6行:首先验证输入是否有效。此时可能会出现错误提示;
  • 第8-9行:清空响应列表。将此变更同步至负责显示响应的ListView
  • 第11-12行:将收到的响应数量清零;
  • 第14行:设置随机数服务器的URL。该信息将传递至[DAO]层;
  • 第15行:设置向随机数服务发起请求前的等待时间。该信息将传递至[DAO]层;
  • 第17行:准备启动1个异步任务(而非N个,稍后将说明原因);
  • 第24-27行:将N个异步任务拆分为一系列操作 [merge];
  • 第29-36行:请求父类[AbstractParent]向Web服务/jSON查询随机数;
  • 第29-36行:方法[executeInBackground]需要两个参数:
    • 第29行:待观察和执行的进程即前几行计算出的那个;
    • 第29-36行:当收到异步服务响应时需执行的[Action1]实例。 [Action1<T>] 的类型 T 必须是方法 [getAlea] 返回结果的类型 T,即类型 [Response<Integer>];
  • 第 34 行:当收到响应(一个随机数)时,在第 39 行的方法中处理该响应;
  • 第 49-50 行:记录并报告已收到新的响应;
  • 第53-60行:类型[Response<T>]有一个字段[status],该字段表示错误代码。如果该代码不为零,则说明服务器遇到了问题;
  • 第55行:显示一条错误消息。方法[showAlert]属于父类;
  • 第 57 行:调用第 68-75 行中的方法。该方法将取消仍在运行的任务(第 74 行);
  • 第 62 行:将响应添加到响应列表中,该列表是 ListView 的数据源;
  • 第 64 行:刷新 ListView
  • 第77-83行:[beginWaiting(int nbRunningTasks)]方法为视图做好就绪准备(第81-82行),并通知父类[nbRunningTasks]任务即将执行(第79行);

2.8.3.7.2. 片段的生命周期

片段的生命周期由以下方法保障:


  // 本地数据
  private List<String> reponses;
  private ArrayAdapter<String> dataAdapterReponses;
  private int nbReponses = 0;
...
  // 生命周期管理 ---------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // 视图当前状态
    Vue1FragmentState state = new Vue1FragmentState();
    state.setTextViewErreurDelayVisible(textViewErreurDelay.getVisibility() == View.VISIBLE);
    state.setTxtErrorAleasVisible(txtErrorAleas.getVisibility() == View.VISIBLE);
    state.setTxtMsgErreurUrlServiceWebVisible(txtMsgErreurUrlServiceWeb.getVisibility() == View.VISIBLE);
    state.setTxtErrorIntervalleVisible(txtErrorIntervalle.getVisibility() == View.VISIBLE);
    state.setBtnExecuterVisible(btnExecuter.getVisibility() == View.VISIBLE);
    state.setReponses(reponses);
    return state;
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // 首次访问?
    if (previousState != null) {
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      reponses = state.getReponses();
    } else {
      reponses = new ArrayList<>();
    }
    // listView 的数据源
    dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    // 回复数
    nbReponses = reponses.size();
  }

  @Override
  protected void initView(CoreState previousState) {
    // 列表视图/适配器链接
    listReponses.setAdapter(dataAdapterReponses);
    // 首次访问?
    if (previousState == null) {
      // 隐藏错误消息
      txtErrorAleas.setVisibility(View.INVISIBLE);
      txtErrorIntervalle.setVisibility(View.INVISIBLE);
      txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
      textViewErreurDelay.setVisibility(View.INVISIBLE);
      // 按钮
      btnAnnuler.setVisibility(View.INVISIBLE);
      btnExecuter.setVisibility(View.VISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // 视图的上一状态
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    // 显示/隐藏错误消息
    txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
    txtErrorIntervalle.setVisibility(state.isTxtErrorIntervalleVisible() ? View.VISIBLE : View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
    textViewErreurDelay.setVisibility(state.isTextViewErreurDelayVisible() ? View.VISIBLE : View.INVISIBLE);
    // 按钮
    btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
    btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
    // 回答数量
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // 按钮 [Exécuter] 替换按钮 [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);

}
  • 第 7-18 行:在父类要求时确保片段被保存;
  • 第 11 行:显示关于超时的错误消息;
  • 第 12 行:显示关于请求随机数数量的错误消息;
  • 第 13 行:显示 Web 服务 /URL /jSON 的超时错误消息;
  • 第14行:显示关于随机数生成间隔[a,b]的错误信息;
  • 第 15 行:[Exécuter] 按钮的可见性;
  • 第16行:已接收的响应列表;
  • 第20-23行:应返回视图编号。此处的片段编号为0,因为只有一个片段;
  • 第25-38行:初始化片段字段,无论是首次访问(previousState==null)还是后续访问;
    • 第29-30行:若非首次访问,则根据片段的先前状态恢复[reponses]字段;
    • 第31-33行:如果是首次访问,则将字段[reponses]初始化为空列表;
    • 第34-37行:基于字段[reponses],可以构建片段中ListView的数据源(第35行)以及响应数量(第37行);
  • 第40-55行:用于初始化与片段关联的视图,无论是在首次访问时(previousState==null),还是在后续访问时;
    • 第 43 行:将片段的 ListView 关联到刚刚在 [initFragment] 方法中构建的数据源;
    • 第45-54行:如果是首次访问,则准备视图以进行首次显示;
  • 第57-60行:在与[SUBMIT]类型操作相关的片段间导航时执行。此处仅有一个片段,因此不存在片段间导航;
  • 第63-76行:在与类型为[NAVIGATION]的操作相关的片段间导航时执行,或在因设备旋转或其他原因导致的备份/恢复循环中执行。 此处仅可能发生后一种情况。需注意,在此处,无论何种情况,[previousState] 始终不等于 null
  • 第 65 行:将上一个状态强制转换为片段状态的类型;
  • 第66-75行:使用前一状态的内容来恢复视图;
  • 第 78-81 行:在所有先前更新完成后调用。此处无需执行任何操作;
  • 第83-89行:当所有异步任务完成时执行。此处将按钮[Annuler]隐藏,并替换为按钮[Exécuter];

2.8.3.8. 测试

请读者进行以下测试:

  • 制造错误并运行设备:错误信息应保留;
  • 获取随机数并运行设备:生成的随机数应保持显示;
  • 设置数秒的等待时间,并在等待期间运行设备:任务应已被取消(可在日志中看到);

2.8.4. 示例-22B

在此我们重新采用示例22,并根据项目[client-android-skel]的模型对其进行重构。 需要指出的是,项目 [Exemple-22] 在旋转过程中正确管理了片段的保存/恢复循环,并且该项目正是项目 [client-android-skel] 的基础。

我们将项目 [client-android-skel] 复制为 [exemples/Exemple-22B],并加载该项目:

  

随后,我们将项目 [Exemple-22] 中的各类元素复制到项目 [Exemple-22B] 中。

首先,我们将 [res] 文件夹中的元素复制过来:

  • [layout/fragment_main.xml, layout/vue1.xml, menu/menu_fragment.xml, menu/menu_main.xml, 文件夹 [values] ;
  

将这两个视图的上边距修改为 120 dp:

[vue1.xml]:


  <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="120dp"
    android:textSize="50sp"
    android:layout_gravity="center|left"
    android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>

[fragment_main]:


  <TextView
    android:id="@+id/section_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
android:layout_marginTop="120dp"/>

接下来,我们将复制 [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState] 中的元素:

 

此时,我们可以尝试首次编译。出现了一类错误:由于类已更改包,导致 imports 存在错误。我们修正这些 imports。 第二类错误是由于片段未实现其父类 [AbstractFragment] 的所有方法所致。通过 (Alt+Enter) 进行修正。

剩余的错误源于旧类与新类 [AbstractFragment] 之间的差异。目前,我们忽略这些错误。

2.8.4.1. 项目自定义

  

在文件夹 [custom] 中,包含开发人员可自定义的架构元素。

通过 [IMainActivity] 界面可指定项目的某些特性:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // 登录
  ISession getSession();

  // 切换视图
  void navigateToView(int position, ISession.Action action);

  // 等待管理
  void beginWaiting();

  void cancelWaiting();

  // 调试模式
  boolean IS_DEBUG_ENABLED = true;

  // 服务器响应的最大等待时间
  int TIMEOUT = 1000;

  // 执行客户端请求前的等待时间
  int DELAY = 0;

  // 基本身份验证
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // 片段邻接性
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // 标签栏
  boolean ARE_TABS_NEEDED = true;

  // 加载图片
  boolean IS_WAITING_ICON_NEEDED = false;

  // 片段数量
  int FRAGMENTS_COUNT = 5;

}
  • 第 23、26、29、38 行:[DAO] 层的特性。此处没有;
  • 第 41 行:此处有五个片段;
  • 第 32 行:片段的邻接关系。此处的常量在 [1,4] 中可能取特定值。建议读者尝试更改该值,以验证应用程序是否仍能正常运行;
  • 第 35 行:这是一个标签页应用程序;

存储片段状态的类 [CoreState] 将如下所示:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.PlaceHolderFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = PlaceHolderFragmentState.class)}
)
public class CoreState {
  // 片段是否已访问
  protected boolean hasBeenVisited = false;
  // 片段菜单(如有)的状态
  protected MenuItemState[] menuOptionsState;

  // 获取器和设置器
...
}
  • 第 12 行:我们声明片段状态类 [PlaceHolderFragment]。而片段 [Vue1Fragment] 则没有状态;

类 [Session] 如下所示:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // 片段之间以及片段与活动之间共享的数据
  // 无法序列化为 jSON 的元素必须带有 @JsonIgnore 注解
  // 切勿遗漏序列化/反序列化所需的 getter 和 setter 方法 jSON

  // 已访问的片段数量
  private int numVisit;
  // 在第二个标签页中显示的 [PlaceholderFragment] 类型的片段编号
  private int numFragment = -1;

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

这是项目 [Exemple-22] 的会话。

2.8.4.2. 活动 [MainActivity]

  

[MainActivity] 活动如下:


package client.android.activity;

import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.util.Log;
import android.view.MenuItem;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.PlaceholderFragment_;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // [DAO] 层
  @Bean(Dao.class)
  protected IDao dao;
  // 会话
  private Session session;

  // 菜单管理-----------------------
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
...
  }

  private void showFragment(int i) {
...
  }

  // 父类方法的实现 ---------------------------------------------------
  ...
}

在此,[MainActivity]类比前面的示例更为复杂,原因有二:

  • 需要管理标签页;
  • 需要管理菜单;

2.8.4.2.1. 父类方法的实现

// 父类方法 -----------------------
  @Override
  protected void onCreateActivity() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // 继续父类已开始的初始化
    // 会话
    this.session = (Session) super.session;
    ...
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // 片段编号
    final String ARG_SECTION_NUMBER = "section_number";
    // 初始化片段数组
    AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
    int i;
    for (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[i] = new Vue1Fragment_();
    // 结果
    return fragments;
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // 此处无标题
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
...
  }

  @Override
  protected int getFirstView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
  • 第 2-12 行:当活动首次创建或在保存/恢复循环中重新创建时,父类 [AbstractActivity] 会调用方法 [onCreateActivity]。调用该方法时,父类已恢复会话;
  • 第 10 行:获取会话的本地引用。类型变更的原因在于父类的会话类型为 [AbstractSession];
  • 第19-38行:方法[getFragments]需将应用程序管理的片段数组返回给父类。此处包含[FRAGMENTS_COUNT]个片段,该数量在[IMainActivity]中定义。 前几个片段的类型为 [FRAGMENTS_COUNT-1],最后一个片段的类型为 [Vue1Fragment];
  • 第41-45行:当该信息有用时,[getFragmentTitle]方法应返回片段的标题。但此处并非如此;
  • 第 47-50 行:当用户点击标签页时,父类会调用此方法。我们将在下一段中详细讨论;
  • 第 52-55 行:返回应用程序启动时要显示的第一个视图的编号。此处应首先显示片段 [Vue1Fragment]。 方法 [getFirstView] 最好用 [IMainActivity] 中的常量来代替;

2.8.4.2.2. 标签页管理

标签页由以下方法管理:


@Override
  protected void onCreateActivity() {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // 继续由父类开始的初始化
    // 会话
    this.session = (Session) super.session;
    // 第一个标签页
    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);
    }
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // 要显示的片段编号
    int numFragment;
    switch (position) {
      case 0:
        // 片段编号 [Vue1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // 片段编号 [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // 片段显示
    if (numFragment != mViewPager.getCurrentItem()) {
      navigateToView(numFragment, ISession.Action.SUBMIT);
    }
  }
}
  • 第 1-20 行:当活动首次创建或在保存/恢复循环中重新创建时,父类 [AbstractActivity] 会调用方法 [onCreateActivity]。调用此方法时,父类已恢复会话;
  • 第 9 行:获取会话的本地引用。类型转换是因为父类的会话类型为 [AbstractSession];
  • 第11-13行:创建第一个标签页;
  • 第15-20行:如果会话中存储了片段编号(第15行),则创建第二个标签页。在首次构建活动时,该编号初始值为-1;
  • 第23-39行:当用户点击某个标签页时,父类会调用此方法;
  • 第28-31行:若点击的是标签页0,则需显示[Vue1Fragment]。已知这是应用程序启动时显示的第一个视图;
  • 第 32-35 行:如果点击的是标签页 1,则应显示其编号已保存在会话中的片段;
  • 第37-39行:导航至所选片段。关联的操作是[SUBMIT]。这难道不应该是[NAVIGATION]吗? 在本文档中,仅当显示新片段时只需了解其先前状态,才会使用 [NAVIGATION]。此处并非如此,因为所显示片段的状态必须与其先前状态不同,以显示多一次访问;

2.8.4.2.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"/>
  <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>

该菜单显示以下内容:

  

菜单管理由以下方法负责:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // 日志
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onOptionsItemSelected");
    }
    // 处理菜单选项
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d(className, "action_settings selected");
        }
        break;
      }
      case R.id.fragment1: {
        showFragment(0);
        break;
      }
      case R.id.fragment2: {
        showFragment(1);
        break;
      }
      case R.id.fragment3: {
        showFragment(2);
        break;
      }
      case R.id.fragment4: {
        showFragment(3);
        break;
      }
    }
    // 处理项
    return true;
  }

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // 在软件选定标签页时不进行导航
      session.setNavigationOnTabSelectionNeeded(false);
      // 因标题字体问题,重新创建这两个标签页
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
      // 待显示片段的编号已设置
      session.setNumFragment(i);
      // 通过导航选择第2个标签页
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • 第16-31行:处理对类型为[Fragmenti]的菜单选项的点击;
  • 第37-50行:在第1个标签页(即第2个标签页)中显示编号为i的片段(即类型为PlaceHolderFragment的片段);
  • 第42-44行:决定删除现有标签页,并重新创建两个新标签页。 做出此决定是为了规避以下问题:当仅在现有的第1个标签页中显示片段(即不删除该标签页)时,奇怪的是其标题的外观(字体、大小)与第0个标签页的标题不同;
  • 第43-44行:创建了两个标签页但未选中(false的最后一个参数);
  • 第40行:第42-44行的操作可能会对标签页执行[select]操作,从而调用[onTabSelected]管理器。如果不采取任何措施,系统将跳转至某个片段。 通过在会话中将布尔值 [navigationOnTabSelectionNeeded] 设置为 faux 可避免此情况。当片段可见时,[AbstractFragment] 类会自动将该布尔值重置为 vrai
  • 第 46 行:将要显示的片段编号记录在会话中;
  • 第 48-50 行:选择带有导航功能的第 2 号标签页(第 48 行)。这将触发 [onTabSelected] 过程,该过程将:
    • 显示已记录到会话中的片段;
    • 将所选标签页的编号存储在会话中;

2.8.4.3. 片段 [Vue1Fragment]

此处提供该片段的最终版本:


package client.android.fragments.behavior;

import android.widget.EditText;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {

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

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

  // 片段的生命周期 -----------------------------------------------
  private void initFragment() {
    // 无需操作
  }

  // 保存片段状态
  @Override
  public CoreState saveFragment() {
    // 视图状态 - 无内容可保存
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // 无需操作
  }

  @Override
  protected void initView(CoreState previousState) {
    // 首次访问?
    if (previousState == null) {
      // 显示访问编号
      showNumVisit();
    }

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // 显示访问编号
    showNumVisit();

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

  // 私有方法 -------------------------------------
  // 显示访问编号
  private void showNumVisit() {
    // 递增访问编号
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // 显示访问次数
    Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}

该类几乎为空。

  • 第 35-39 行:当片段需要保存其状态时,由父类调用。 片段 [Vue1Fragment] 无需保存状态。只需返回基类 [CoreState] 的实例(提醒:切勿返回 null);
  • 第 41-44 行:必须返回片段编号。片段 [Vue1Fragment] 根据构造逻辑具有编号 [FRAGMENTS_COUNT-1];
  • 第51-59行:当片段首次构建时(previousState==null)或后续构建时(previousState!=null),由父类调用;
    • 第54-57行:如果是首次访问,则递增访问次数并显示(第85-92行);
  • 第61-65行:当片段将与操作[SUBMIT]关联显示时调用。此时会递增访问次数并显示该数值。在此情况下,访问次数在生命周期内不可能被递增两次。 实际上,对片段 [Vue1Fragment] 的首次访问发生在应用程序启动时,此时会话中该操作默认值为 [NONE]。这确保了方法 [updateOnSubmit] 不会被调用。 此后,将不再是首次访问,方法 [initView] 不会执行任何操作;
  • 第68-71行:在备份/恢复循环中被调用。由于该片段没有状态,因此此处无需恢复任何内容;
  • 第 73-76 行:在所有先前更新完成后被调用。此处已无其他操作;
  • 第 78-81 行:在所有已启动的异步任务均完成时被调用。此处没有异步任务;

2.8.4.4. 状态 [PlaceHolderFragmentState]

片段 [PlaceHolderFragment] 的状态如下:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class PlaceHolderFragmentState extends CoreState {
  // 文本
  private String text;

  // 构造函数
  public PlaceHolderFragmentState() {

  }

  public PlaceHolderFragmentState(String text) {
    super();
    this.text = text;
  }

  // 获取器和设置器
 ...
}
  • 当需要保存片段状态时,将保存其显示的文本(第7行);

2.8.4.5. 片段 [PlaceHolderFragment]

片段 [PlaceHolderFragment] 将变为:


package client.android.fragments.behavior;

import android.util.Log;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.PlaceHolderFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {

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

  // 数据
  private String text;

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

  // 父类方法的实现 ----------------------------
  @Override
  public CoreState saveFragment() {
    // 保存片段状态
    PlaceHolderFragmentState placeHolderFragmentState = new PlaceHolderFragmentState();
    placeHolderFragmentState.setText(textViewInfo.getText().toString());
    return placeHolderFragmentState;
  }

  @Override
  protected int getNumView() {
    return getArguments().getInt(ARG_SECTION_NUMBER) - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // 原始文本
    text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // 更新显示文本
    // 增加访问次数
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // 修改后的文本
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
    // 日志
    if (isDebugEnabled) {
      Log.d(className, String.format("updateForSubmit, numvisit=%s, texte affiché=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
    }
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // 恢复显示文本
    PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
    textViewInfo.setText(state.getText());

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

}
  • 第30-36行:当父类要求片段保存其状态时,将保存片段显示的文本(第34行);
  • 第38-41行:返回片段编号。该编号取决于创建时作为参数传递的章节编号;
  • 第43-47行:在片段首次构建时调用(previousState==null),或在后续构建时调用(previousState !=null);
    • 第46行:此处未利用之前的状态。首次访问时显示的初始文本 [text](第24行)每次都会重新计算。这种做法值得商榷。本可以将该信息也放入片段的状态中;
  • 第49-51行:在首次构建与片段关联的视图时(previousState==null)或后续构建时(previousState!=null)被调用。无需进行任何操作;
  • 第53-56行:当片段将与操作[SUBMIT]关联显示时被调用。除保存/恢复循环(此时操作为[RESTORE])外,始终如此。因此我们递增访问编号并将其显示;
  • 第68-74行:在保存/恢复循环中调用。将之前保存在片段状态中的文本恢复;
  • 第76-79行:在所有先前更新完成后被调用。此处无需执行其他操作;
  • 第 82-83 行:在所有已启动的异步任务均完成后被调用。此处没有异步任务;

2.8.4.6. Tests

建议读者通过旋转设备来测试该应用程序,以验证显示的片段是否会丢失其状态。同时,我们还将查看日志。

2.9. Conclusion

本章结束时,我们已获得一个名为 [client-android-skel] 的 Android 客户端示例项目,该项目与 Web 服务 / jSON 进行通信,具有以下特征:

  • 通过 RxJava 库与 Web 服务器 / jSON 进行异步通信;
  • 片段的生命周期(更新、保存、恢复)由其父类 [AbstractFragment] 管理,该父类会在特定时刻调用其子类的某些方法。因此,子片段无需关注生命周期的各个阶段,只需实现父类要求的特定方法即可;
  • 活动的生命周期(保存/恢复)由抽象类 [AbstractActivity] 管理,该类同样要求子活动实现某些方法;
  • 类 [AbstractActivity] 能够管理带或不带标签页、带或不带加载图片、以及是否需要通过 Web 服务器 / jSON 进行基本身份验证的应用程序。这些元素的启用与否通过配置决定;

接下来我们将介绍一个比之前示例更复杂的案例研究。新应用程序将基于模板项目 [client-android-skel]。