> For the complete documentation index, see [llms.txt](https://xxgqin.gitbook.io/android/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://xxgqin.gitbook.io/android/ch08/ch08-1.md).

# 第十四个项目——新闻列表3

## 实验目的

* 掌握**okhttp**网络库进行同步、异步网络请求的方法；
* 掌握**json**数据格式及**gson**库的使用方法；
* 掌握第三方数据**API**接口申请及使用方法；

## 实验要求

* 使用**okhttp**访问第三方数据API接口；
* 使用**gson**库进行**json**数据解析及展示；
* 使用多线程进行网络访问及数据处理；

## 实验内容

在[第七个项目](https://xxgqin.gitbook.io/android/ch02/ch02-4)及[第九个项目](https://xxgqin.gitbook.io/android/ch04/ch04-2)中使用**ListView**实现新闻列表，这两个项目中的新闻列表数据分别从字符串资源文件**arrays.xml**或**SQLite**数据库进行读取。 在实际应用项目中，数据往往存储于云端服务器中，通过**http restful**接口等形式进行获取，数据的格式包括**json**、**xml**等形式。通常的做法是每一类数据对应一个**API**接口，**App**通过**http**请求后，获取响应的数据后进行解析并与相应的**UI**控件进行数据绑定从而完成数据展示。

**http restful**接口通常需要根据项目需求自行设计开发并部署至云端服务上，以便**App**能够进行访问。在本项目中，我们使用开放第三方**API**接口服务，这样省去了进行接口开发的环节，便于我们聚焦项目的功能要求。 目前有较多第三方**API**接口服务商，提供不同类型的**API**接口服务，例如：聚合数据、天行数据、极速数据等。本项目中使用天行数据提供的综合新闻**API**获取新闻列表，并通过**WebView**控件展示指定新闻的详情页，项目运行效果如[图1. 新闻列表页](/android/ch08/ch08-1.md#code14_list)所示。

![图1. 新闻列表页](http://www.funnycode.net/guet/img/ch08/Code14_running_screenshot.png)

![图2. 新闻详情页](http://www.funnycode.net/guet/img/ch08/Code14_detail_screenshot.png)

### 步骤一，注册天行数据账号获取APIKEY

**1. 注册账号并获取APIKEY**

通过Web浏览器打开天行数据官网<https://www.tianapi.com>，点击页面右上角【立即注册】，填写注册信息后进入天行数据控制台在【账号中心】查看**APIKEY**。**APIKEY**是使用天行数据**API**接口时需要附带的一个标识字符串，用于做接口鉴权，具体使用方式在后续步骤中进行介绍。

![图3. 天行数据官网首页](http://www.funnycode.net/guet/img/ch08/tianapi_index_screenshot.png)

![图4. 天行数据控制台](http://www.funnycode.net/guet/img/ch08/tianapi_console_screenshot.png)

**2. 测试新闻API接口**

在本项目中需要使用到综合新闻的**API**接口，首先通过天行数据提供的网页版接口测试工具熟悉接口的调用参数及**json**数据返回格式。

返回天行数据官网首页，点击【接口】菜单进入**API**可用接口列表，选择【综合新闻】接口，进入该接口描述介绍页面， 如[图5. 综合新闻API接口文档页](/android/ch08/ch08-1.md#tianapi_api_doc)所示。接口描述页面包含对该接口使用、价格、请求参数、返回示例以及参考代码等介绍，可以快速帮助开发者熟悉接口的使用方法。

此外，点击【在线测试】按钮可进入**HTTP**请求在线测试工具页面，通过该页面可动态填写接口的请求地址、请求参数等，并在网页端查看测试的返回内容便于开发者直观的了解和使用接口。

例如[图6. 综合新闻API在线测试工具](/android/ch08/ch08-1.md#tianapi_api_web_tool)中，进行的HTTP请求地址及信息分别是：

\>

> 请求地址: <http://api.tianapi.com/generalnews/> 请求方式: GET 请求参数: key:注册后申请的APIKEY num:接口分页时每页包含的新闻数量 page:当前页数

使用网络请求库（在第\ref{sec:okhttp}章将介绍使用**okhttp**库进行网络请求）时，根据上述信息生成**HTTP**请求头(**Request header**) 及请求体(**Request body**)，并向服务器发送**HTTP**请求并获取**HTTP**响应(**Response**)数据。 在本例中，使用**GET**方法进行**HTTP**请求，则所有请求参数将附加在请求地址后，因此实际请求**URL**为:

\>

> <http://api.tianapi.com/generalnews/?page=1&num=10&key=注册的APIKEY>

云服务器各接口通过处理**HTTP**请求，并返回对应**HTTP**响应。**HTTP**响应包含**响应头部**(**Response header**)和**响应体**(**Response body**)。 网络请求库提供相应的接口解析**HTTP**响应，**App**只需要在判断**HTTP**响应正确(**Response code**为200)的情况下，解析**HTTP**响应体即可获得云服务器接口返回的数据。天行数据的接口**HTTP**响应体为**json**格式，因此只要正确解析该格式即可获得所需数据并在**Activity**中进行展示。

```markup
    HTTP响应头部

    HTTP/1.1 200 OK
    Server: nginx
    Date: Thu, 12 Sep 2019 03:41:21 GMT
    Content-Type: application/json;charset=utf8
    Transfer-Encoding: chunked
    Connection: keep-alive
    Access-Control-Allow-Origin: *
    Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
    ...
```

以下为例，接口返回HTTP响应体的**json**对象，该对象包含三个键值对：

* *code*，表示请求返回代码，与**Response code**不同，用于表示实际请求是成功或是错误；
* *msg*，表示与*code*对应的消息，例如*success*表示成功；
* *newslist*，**json**对象数组，为实际返回的数据，需要进行解析；

{% hint style="info" %}
不同接口返回的json对象结构不同，具体看接口的文档描述
{% endhint %}

```javascript
    HTTP响应体:

    {
      "code": 200,
      "msg": "success",
      "newslist": [
        {
          "ctime": "2019-09-07 16:49",
          "title": "阿里巴巴20周年给家乡的一封信：谢谢你，杭州",
          "description": "网易互联网",
          "picUrl": "http://cms.ws.126.net/2019/09/07/842bf50.png,
          "url": "https://tech.163.com/19/0907/16/EOG30A9LD.html"
        },
        {
          "ctime": "2019-09-07 17:43",
          "title": "2018年度中国零售百强：7家企业销售规模过千亿",
          "description": "网易互联网",
          "picUrl": "http://cms.ws.126.net/2019/09/07/4e5e26a.png,
          "url": "https://tech.163.com/19/0907/17/EOC097U7R.html"
        },
        ...
        ]
    }
```

![图5. 综合新闻API接口文档页](http://www.funnycode.net/guet/img/ch08/tianapi_api_doc.png)

![图6. 综合新闻API在线测试工具](http://www.funnycode.net/guet/img/ch08/tianapi_api_web_tool.png)

### 步骤二，新建名为GGNews的项目

在**Android Studio**新建名为**GGNews**的项目。

**1. 添加第三方库依赖**

打开项目**app**模块中的**build.gradle**文件，新增**okhttp**（网络请求库）、 **gson**（json解析库）、**glide**（图片加载库）三个第三方库的依赖，代码如下所示。

```java
apply plugin: 'com.android.application'

  ...

  dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.android.support:cardview-v7:28.0.0'
    implementation 'com.android.support:design:28.0.0'

    implementation 'com.squareup.okhttp3:okhttp:3.10.0'
    implementation 'com.google.code.gson:gson:2.7'

    implementation 'com.github.bumptech.glide:glide:4.7.1'`
    ...
  }
```

**Android Studio**会检测所&#x6709;**\*.gradle**文件内容，一旦其发生修改便会在**IDE**中提醒用户同步项目依赖库， 如[图7. 同步项目依赖库](/android/ch08/ch08-1.md#code14_gradle_sync)所示，直接点击**Sync Now**即可。 不同第三方库对于**Android SDK**版本依赖不一样。如何确定与你所选择的**Android SDK**对应的第三方库的 版本，可查看第三方库的官方文档。本项目中，使用的**Android SDK**版本为28，而第三方库的版本分别如下：

* okhttp库版本：3.10.0；
* gson库版本：2.7；
* glide库版本：4.7.1；

![图7. 同步项目依赖库](http://www.funnycode.net/guet/img/ch08/Code14_gradle_sync.png)

**2. 配置AndroidManifest.xml文件**

为**GGNews**项目配置网络访问权限，打开**AndroidManifest.xml**文件，新增权限声明，代码如下所示。

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

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

  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:networkSecurityConfig="@xml/network_security_config"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".DetailActivity"></activity>
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>
```

此外，从**Android P**（API Level 28）版本起，要求**App**进行网络请求时使用**https**协议进行加密网络请求，但天行数据API目前仍使用**http**协议进行明文网络请求，因此需新增名为**network\_security\_config.xml**声明文件允许进行明文网络请求。首先右击**app/src/res**文件夹，选择【New】->【Directory】新建名为**xml**的文件夹，并在新建的**xml**文件夹中新建名为**network\_security\_config.xml**文件，其内容如下代码所示，并在**AndroidManifest.xml**中**application**标签内引用**network\_security\_config.xml**配置。

```markup
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>
```

### 步骤三，MainActivity及布局

**1. 设计activity\_main.xml布局**

实现新闻列表页，需要在**activity\_main.xml**中加入**ListView**控件，并定义**ListView**控件的**Item**布局，其中**activity\_main.xml**布局如下代码所示。

```markup
<?xml version="1.0" encoding="utf-8"?>
  <android.support.constraint.ConstraintLayout
      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"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      tools:context=".MainActivity">

      <ListView
          android:id="@+id/lv_news_list"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:layout_margin="8dp"
          android:scrollbars="none"
          android:divider="@android:color/transparent"
          android:dividerHeight="8dp"
          app:layout_constraintTop_toTopOf="parent"
          app:layout_constraintEnd_toEndOf="parent"
          app:layout_constraintStart_toStartOf="parent"
          app:layout_constraintBottom_toBottomOf="parent"
          />

</android.support.constraint.ConstraintLayout>
```

**2. 新建list\_item.xml布局**

要实现如[图1. 新闻列表页](/android/ch08/ch08-1.md#code14_list)所示的新闻列表页效果，需要定义**ListView**控件的**Item**布局，这里定义其布局文件名为**list\_item.xml**，每一项**Item**需要显示标题、新闻来源、日期、标题图等信息，根据需要选择相应的视图控件，具体如下代码所示。

```markup
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_width="match_parent"
        android:layout_height="112dp">

    <ImageView
        android:id="@+id/iv_image"
        android:layout_width="96dp"
        android:layout_height="64dp"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:layout_alignParentBottom="true"
        android:layout_alignParentTop="true"
        android:scaleType="centerCrop"
        android:layout_gravity="center"
        />

    <TextView
        android:id="@+id/tv_title"
        style="@style/TextAppearance.AppCompat.Subhead"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toEndOf="@id/iv_image"
        android:layout_alignTop="@id/iv_image"
        android:layout_alignParentEnd="true"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:textSize="16sp"
        />

    <TextView
        android:id="@+id/tv_subtitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignStart="@id/tv_title"
        android:layout_alignBottom="@id/iv_image"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="8dp"
        android:textSize="12sp"
        android:textColor="@android:color/secondary_text_dark"
        style="@style/TextAppearance.AppCompat.Subhead"/>

    <TextView
        android:id="@+id/tv_publish_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="12sp"
        android:text="2019-07-11"
        android:layout_marginStart="8dp"
        android:layout_marginBottom="8dp"
        android:textColor="@android:color/secondary_text_dark"
        android:layout_alignBaseline="@id/tv_subtitle"
        android:layout_toEndOf="@id/tv_subtitle"
        android:layout_alignBottom="@id/iv_image"
        />

    <ImageView
        android:id="@+id/iv_delete"
        android:src="@drawable/ic_close_gray_24dp"
        android:layout_alignBottom="@id/iv_image"
        android:layout_alignParentEnd="true"
        android:scaleType="centerCrop"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:clickable="true"
        android:layout_width="16dp"
        android:layout_height="16dp" />

        <View
            android:id="@+id/v_divider"
            android:layout_alignParentStart="true"
            android:layout_alignParentEnd="true"
            android:layout_alignParentBottom="true"
            android:background="@color/colorTransparent"
            android:layout_width="wrap_content"
            android:layout_height="1dp"/>

    </RelativeLayout>
</android.support.constraint.ConstraintLayout>
```

### 步骤四，定义News新闻类及NewsAdapter适配器

**1. 定义News新闻类**

**News**新闻类实例表示一条新闻，该类的属性应该与天行数据的【综合新闻】接口返回*newslist*对象数组中的结构对应，当**App**通过**okhttp**网络库从服务端获得**HTTP**响应后，利用**gson**库解析*newslist*对象数组后，数组中的每一个**json**对象将被转化为一个**News**新闻类实例（这一过程称为解序列化）。

{% hint style="info" %}
将一个Java对象转化为**json**对象字符串的过程称为序列化。
{% endhint %}

**News**新闻类包括的类成员需要覆盖*newslist*对象数组中**json**对象所含的属性，此外还可包含其他额外属性。

```javascript
{
  "ctime": "2019-09-07 16:49",
  "title": "阿里巴巴20周年给家乡的一封信：谢谢你，杭州",
  "description": "网易互联网",
  "picUrl": "http://cms.ws.126.net/2019/09/07/842bf50.png,
  "url": "https://tech.163.com/19/0907/16/EOG30A9LD.html"
}
```

以下为例，一条新闻包含：

* ctime，创建时间；
* title，新闻标题；
* description，新闻来源；
* picUrl，新闻标题图URL；
* url，新闻详情URL；

**gson**库会将一个**json**对象字符串默认按照属性名进行解序列化成指定的Java类对象（在本例中是**News**类）。如果Java类中指定的属性名与**json**对象字符串的属性名不同，可通&#x8FC7;**@SerializedName**注解进行指定；此外，还可通&#x8FC7;**@Expose**注解指定对应的Java类属性是否进行序列化或解序列化，具体如下代码所示。

```java
public class News {
    public String getTitle() {
        return mTitle;
    }

    public void setTitle(String mTitle) {
        this.mTitle = mTitle;
    }

    public String getSource() {
        return mSource;
    }

    public void setSource(String mSource) {
        this.mSource = mSource;
    }

    public String getPicUrl() {
        return mPicUrl;
    }

    public void setPicUrl(String mPicUrl) {
        this.mPicUrl = mPicUrl;
    }

    public String getContentUrl() {
        return mContentUrl;
    }

    public void setContentUrl(String mContentUrl) {
        this.mContentUrl = mContentUrl;
    }

    public Integer getId() {
        return mId;
    }

    public String getDate() {
        return mPublishTime;
    }

    public News() {
    }

    @Expose(serialize = false, deserialize = false)
    private Integer mId;

    @SerializedName("title")
    private String mTitle;

    @SerializedName("description")
    private String mSource;

    @SerializedName("picUrl")
    private String mPicUrl;

    @SerializedName("url")
    private String mContentUrl;

    @SerializedName("ctime")
    private String mPublishTime;

    ...

}
```

**2. 定义NewsAdapter适配器类**

**NewsAdapter**适配器用于将新闻数据渲染加载进入**ListView**控件中进行显示，这步操作已经在其他项目中介绍过多次，在此不进行赘述，具体如下代码所示。

```java
public class NewsAdapter extends ArrayAdapter<News> {

    private List<News> mNewsData;
    private Context mContext;
    private int resourceId;

    public NewsAdapter(Context context, 
                        int resourceId, List<News> data) {
        super(context, resourceId, data);
        this.mContext = context;
        this.mNewsData = data;
        this.resourceId = resourceId;
    }

    @Override
    public View getView(int position, 
                         View convertView, ViewGroup parent) {
        News news = getItem(position);
        View view ;

        final ViewHolder vh;

        if (convertView == null) {
            view = LayoutInflater.from(getContext())
                        .inflate(resourceId, parent, false);

            vh = new ViewHolder();
            vh.tvTitle  = view.findViewById(R.id.tv_title);
            vh.tvSource = view.findViewById(R.id.tv_subtitle);
            vh.ivImage = view.findViewById(R.id.iv_image);
            vh.ivDelete = view.findViewById(R.id.iv_delete);
            vh.tvPublishTime = view.findViewById(R.id.tv_publish_time);

            view.setTag(viewHolder);
        } else {
            view = convertView;
            vh = (ViewHolder) view.getTag();
        }

        vh.tvTitle.setText(news.getTitle());
        vh.tvSource.setText(news.getSource());
        vh.ivDelete.setTag(position);
        vh.tvPublishTime.setText(news.getDate());

        Glide.with(mContext).load(news.getPicUrl())
                .into(viewHolder.ivImage);

        return view;
    }

    class ViewHolder {
        TextView tvTitle;
        TextView tvSource;
        ImageView ivImage;
        TextView tvPublishTime;

        ImageView ivDelete;
    }
}
```

**3. 定义Constants静态类**

定义名为**Constants**的静态类，主要包含需在项目中经常使用到的各类字符串常量或其他类型常量，例如服务器地址、**API**接口名、**APIKEY**等。

```java
public final class Constants {
  private Constants() {
  }

  public static final int NEWS_NUM = 10;
  public static String SERVER_URL = "http://api.tianapi.com/";
  public static String ALL_NEWS_PATH = "allnews/";
  public static String GENERAL_NEWS_PATH = "generalnews/";

  public static String API_KEY = "你的APIKEY";

  public static String ALL_NEWS_URL = SERVER_URL + ALL_NEWS_PATH;
  public static String GENERAL_NEWS_URL = SERVER_URL + GENERAL_NEWS_PATH;

  public static int NEWS_COL5 = 5;
  public static int NEWS_COL7 = 7;
  public static int NEWS_COL8 = 8;
  public static int NEWS_COL10 = 10;
  public static int NEWS_COL11 = 11;

  public static String NEWS_DETAIL_URL_KEY = "news_detail_url_key";
}
```

**4. 定义**BaseResponse**类**

与**News**新闻类对应**HTTP**响应体中*newslist* **json**对象数组中的单个元素类似，由于**HTTP**响应体整体来看就是一个**json**对象字符串，因此需要定义一个与其对应的**Java**类进行解序列化，在此定义名为**BaseResponse**的**Java**类。而该类的属性应与**HTTP**响应体的属性对应。

我们将**BaseResponse**类定义为模板类，该类包含了三个属性，分别为：

* int code，对应**HTTP**响应体中的**code**字段；
* String msg，对应**HTTP**响应体中的**msg**字段；
* T data，对应于**HTTP**响应体中的**newslist**字段；

其中**data**属性定义为模板类型，主要因为在进行**API**接口设计时**HTTP**响应体中通常包含的**code**、**msg**两个字段有固定作用（API接口请求状态及消息），而第三个字段则因**API**接口业务而异，有可能在接口A中返回的是一个**json**数组而在接口B中返回的则是**json**对象，如果每一个接口都需要定义特定的**Response**类，则不利于代码维护。

{% hint style="info" %}
通过将**BaseResponse**类定义为模板类，当接口A需要的是**json**数组，则在实例化**BaseResponse**时指定其模板类型为**List**即可，当接口B需要的是**json**对象时，则实例化**BaseResponse**时指定其模板类型为指定的某一**Java**类即可（例如**News**类）。
{% endhint %}

```java
  public class BaseResponse <T> {
      private int code;
      private String msg;

      public final static int RESPONSE_SUCCESS = 0;

      @SerializedName("newslist")
      private T data;

      public BaseResponse() {
      }

      public int getCode() {
          return code;
      }

      public void setCode(int code) {
          this.code = code;
      }

      public String getMsg() {
          return msg;
      }

      public void setMsg(String msg) {
          this.msg = msg;
      }

      public T getData() {
          return data;
      }

      public void setData(T data) {
          this.data = data;
      }
  }
```

### 步骤五，修改MainActivity活动类

在定义了**BaseResponse**响应体基类后，如何使用**okhttp**网络请求库向天行数据**API**接口请求数据并解析数据的工作放在**MainActivity**活动类中。

**1. 初始化MainActivity布局**

在**MainActivity**活动类的**onCreate()**&#x65B9;法中调用**initView()**&#x79C1;有方法初始化其布局，主要进行**ListView**视图控件绑定以及适配器绑定等操作，具体代码如下所示。

```java
public class MainActivity extends AppCompatActivity {

    private ListView lvNewsList;
    private List<News> newsData;

    private NewsAdapter adapter;

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
        ...
    }

    private void initView() {
        lvNewsList = findViewById(R.id.lv_news_list);

        lvNewsList.setOnItemClickListener(
                              new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, 
                                    View view, int i, long l) {

                Intent intent = new Intent(MainActivity.this,
                                           DetailActivity.class);

                News news = adapter.getItem(i);
                intent.putExtra(Constants.NEWS_DETAIL_URL_KEY, 
                                    news.getContentUrl());

                startActivity(intent);
            }
        });
    }

    ...
}
```

**2. 通过**okhttp**请求**API**接口数据**

为了简便起见，在**MainActivity**活动类的**onCreate()**&#x65B9;法中，完成布局初始化后，直接使用**okhttp**请求**API**接口数据。 首先定义名为**initData()**&#x7684;方法，在该方法中将初始化**List**列表对象用于保存从网络获取的新闻列表信息。其次，调用 **refreshData()**&#x65B9;法进行实际的**API**请求，具体代码如下所示。

```java
public class MainActivity extends AppCompatActivity {
    private ListView lvNewsList;
    private List<News> newsData;

    private int page = 1;

    private int mCurrentColIndex = 0;

    private int[] mCols = new int[]{Constants.NEWS_COL5,
        Constants.NEWS_COL7, Constants.NEWS_COL8,
        Constants.NEWS_COL10, Constants.NEWS_COL11};

    private okhttp3.Callback callback = new okhttp3.Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
          Log.e(TAG, "Failed to connect server!");
          e.printStackTrace();
        }

        @Override
        public void onResponse(Call call, Response response)
                throws IOException {
          if (response.isSuccessful()) {
            final String body = response.body().string();

            runOnUiThread(new Runnable() {
              @Override
              public void run() {
                Gson gson = new Gson();
                Type jsonType = 
                    new TypeToken<BaseResponse<List<News>>>() {}.getType();
                BaseResponse<List<News>> newsListResponse = 
                    gson.fromJson(body, jsonType);
                for (News news:newsListResponse.getData()) {
                  adapter.add(news);
                }

                adapter.notifyDataSetChanged();
              }
            });
          } else {
          }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
        initData();
    }

    ... 

    private void initData() {
        newsData = new ArrayList<>();
        adapter = new NewsAdapter(MainActivity.this,
                            R.layout.list_item, newsData);
        lvNewsList.setAdapter(adapter);

        refreshData(1);
    }

    private void refreshData(final int page) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                NewsRequest requestObj = new NewsRequest();

                requestObj.setCol(mCols[mCurrentColIndex]);
                requestObj.setNum(Constants.NEWS_NUM);
                requestObj.setPage(page);
                String urlParams = requestObj.toString();

                Request request = new Request.Builder()
                        .url(Constants.GENERAL_NEWS_URL + urlParams)
                        .get().build();
                try {
                    OkHttpClient client = new OkHttpClient();
                    client.newCall(request).enqueue(callback);
                } catch (NetworkOnMainThreadException ex) {

                    ex.printStackTrace();
                }
            }
        }).start();
    }
}
```

**Android**系统不允许在主线程中进行网络请求操作，因此在**refreshData()**&#x65B9;法中创建了子线程，并在子线程中进行网络请求。进行网络请求需要使用**okhttp**网络请求库，其核心是实例化**OkHttpClient**对象，并调用该对象的**newCall()**&#x53CA;enqueue()\*\*方法进行异步请求。

**newCall()**&#x65B9;法需要接受**Request**类型的参数，该参数主要包括对所需请求的**URL**地址、请求方法等进行设置。实例化**Request**对象，需要使用**Request.Builder**静态类的build()方法最终生成**Request**请求对象。在代码\ref{lst:code14\_init\_data}中通过调用**Request.Builder**类的**url()**、**get()**&#x7B49;方法设置了生成**Request**请求对象的相关属性。

{% hint style="info" %}
如果需要使用**POST**方式向**API**接口请求数据，则可调用**post()**&#x65B9;法设置**RequestBody**请求体。
{% endhint %}

在本项目中使用http **GET**方式进行**API**接口请求，因此在**url()**&#x65B9;法中将天行数据【综合新闻】的**API**的**url**地址与请求参数 进行字符串拼接即可，请求参数通过**NewsRequest**新闻请求类进行构造，**NewsRequest**新闻类的定义具体见本节下一个步骤。

**OkHttpClient**类的**enqueue()**&#x65B9;法接收一个**okhttp3.Callback**接口作为**异步Get**(**Asynchronous Get**)。**Callback**接口包含两个方法，当**http**请求响应或失败时被分别调用：

* **onResponse(Call call, Response response)**，当**http**请求有响应时被调用，通过**Response**对象的**isSuccessful()**&#x65B9;法可判断请求是否成功；
* **onFailure(Call call, IOException e)**，当**http**请求失败时被调用，可通过**Call**对象重新进行请求或取消请求，通过**IOException**对象可获取出错的原因。

在**MainActivity**中定义了**okhttp3.Callback**接口对象，其**onResponse()**&#x65B9;法中首先通过**Response.body()**&#x65B9;法取得请求响应体对象并其转换成**String**类型，也就是**body**字符串对象。此时**body**字符串对象保存的是服务器请求相应体的数据，该数据为**json**字符串对象格式。

如果**Response.isSuccessful()**&#x65B9;法返回为**true**，则此时**body**对象保存的是在本实验第一步中举例的**HTTP**响应体。只要将其正确解析即可获得所需的新闻列表数据。因此在**onResponse()**&#x65B9;法中的**runOnUiThread()**&#x65B9;法中的代码就是用于完成将**HTTP**响应体解析并获得新闻列表数据的每一条新闻信息。

**runOnUiThread()**&#x65B9;法接收一个**Runnable**接口对象作为参数，该接口中的**run()**&#x65B9;法将在**UI**主线程中被执行。

{% hint style="info" %}
此时的**okhttp3.Callback**对象的**onResponse()**、**onFailure()**&#x4E24;个方法在**refreshData()**&#x65B9;法所开启的子线程中被执行。对于**UI**视图组件对象的操作必须在**UI**主线程中。
{% endhint %}

在该方法中首先通过**Gson**、**TypeToken**两个类型用于定义**BaseResponse**所需的正确解析模板类型，并通过**Gson.fromJson()**&#x65B9;法将**Response.body()**&#x83B7;取到的**HTTP**响应体正确解析为所需的**BaseResponse**类型。

此时通过**BaseResponse.getData()**&#x65B9;法即可获得**HTTP**响应体的*newslist*新闻列表数组，且经过**Gson**解序列化后直接转化成了**List**列表，最后可通过*for*循环遍历该列表，把列表中的每一个**News**新闻对象添加到**ListView**所绑定的**NewsAdapter**适配器中，最后通过适配器的**notifyDataSetChanged()**&#x65B9;法通知**ListView**刷新数据，从而完成从天行数据【综合新闻】**API**接口进行请求、解析数据并最终在**MainActivity**活动的**ListView**控件显示的操作。

**3. 定义NewsRequest新闻请求类**

由于每一个**API**接口其所需的请求参数均有可能不同，因此通常需要针对每一个**API**接口定义与之对应的请求类用于构造相应的请求参数。

{% hint style="info" %}
在实际应用开发中你可能需要定义一个请求类的基类，并从该基类派生出与每一个**API**对应的请求类。
{% endhint %}

在本例中，由于仅需访问【综合新闻】一个**API**接口，因此根据该接口所需的参数，定义一个名为**NewsRequest**的请求类，具体代码如下所示。

```java
public class NewsRequest {
    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public int getCol() {
        return col;
    }

    public void setCol(int col) {
        this.col = col;
    }

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }

    public int getRand() {
        return rand;
    }

    public void setRand(int rand) {
        this.rand = rand;
    }

    public String getKeyword() {
        return keyword;
    }

    public void setKeyword(String keyword) {
        this.keyword = keyword;
    }

    @Override
    public String toString() {
        String retValue;

        retValue = "?" + "&key=" + Constants.API_KEY
                    + "&num=" + num + "&col=" + col;
        if (page != -1) {
            retValue += "&page=" + page;
        }
        return retValue;
    }

    private int num;
    private int col;
    private int page = -1;
    private int rand;
    private String keyword;
}
```

【综合新闻】**API**接口接受如下几个参数：

* key，API密钥，注册后用户唯一的APIKEY，必填；
* num，返回数据数量，必填；
* page，翻页的页数；
* rand，是否随机获取；
* word，检索关键词；

**NewsRequest**本质上是一个普通的**Java**类，在此我们重载了其**toString()**&#x65B9;法，便于将其属性转换为**API**接口所需的参数。

{% hint style="info" %}
由于在此例子中，我们使用http **GET**方式请求**API**接口，因此直接将**NewsRequest**的属性通过**toString()**&#x65B9;法拼接成参数字符串。 如果使用http **POST**方法，并且指定请求体为json数据格式，则需要**gson**等第三方解析库将**NewsRequest**对象进行序列化。
{% endhint %}

### 步骤六，编译项目并部署应用

完成上述五个步骤后，通过**Android Studio**编译本项目并将应用部署于虚拟机或物理机等测试设备上，App启动后 查看其是否在**ListView**中正确显示所获取的新闻列表。

## 实验小结

通过本次实验，你应该掌握了如下知识内容：

* 使用**Thread**、**Runnable**启动子线程；
* 使用**okhttp**进行网络请求；
* 使用**Gson**进行**Java**对象序列化及解序列化；


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://xxgqin.gitbook.io/android/ch08/ch08-1.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
