✏️
Android应用开发
  • 简介
  • 目录
  • 第一章 熟悉Android Studio开发环境
    • 第一个项目——Hello World!
    • 第二个项目——计数器
    • 第三个项目——新闻阅读器
  • 第二章 活动、组件及布局
    • 第四个项目——消息发送
    • 第五个项目——计数器2
    • 第六个项目——登陆界面
    • 第七个项目——新闻列表
  • 第三章 数据持久化
    • 第八个项目——登陆界面2
    • 第九个项目——新闻列表2
  • 第四章 内容提供器与数据共享
    • 第十个项目——音乐播放器1
  • 第五章 多媒体与服务
    • 第十一个项目——音乐播放器2
    • 第十二个项目——音乐播放器3
  • 第六章 绑定服务与自定义广播
    • 第十三个项目——音乐播放器4
  • 第七章 网络与多线程
    • 第十四个项目——新闻列表3
  • 附录 扩展项目
    • 第十五个项目——使用ViewHolder提升ListView显示性能
    • 第十六个项目——使用SwipeRefreshLayout实现下拉刷新
    • 第十七个项目——使用接口回调实现删除ListView列表中的Item数据
    • 第十八个项目——使用RecyclerView显示列表数据
    • 第十九个项目——使用Fragment组织UI页面
    • 第二十个项目——使用CursorAdapter进行数据绑定及渲染
    • 第二十一个项目——使用Loader实现异步数据加载操作
    • 第二十二个项目——使用TimerTask及Handler实现秒表计时器
    • 第二十三个项目——使用AsyncTask进行网络异步请求
Powered by GitBook
On this page
  • 实验目的
  • 实验要求
  • 实验内容
  • 步骤一,注册天行数据账号获取APIKEY
  • 步骤二,新建名为GGNews的项目
  • 步骤三,MainActivity及布局
  • 步骤四,定义News新闻类及NewsAdapter适配器
  • 步骤五,修改MainActivity活动类
  • 步骤六,编译项目并部署应用
  • 实验小结

Was this helpful?

  1. 第七章 网络与多线程

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

Previous第七章 网络与多线程Next附录 扩展项目

Last updated 4 years ago

Was this helpful?

实验目的

  • 掌握okhttp网络库进行同步、异步网络请求的方法;

  • 掌握json数据格式及gson库的使用方法;

  • 掌握第三方数据API接口申请及使用方法;

实验要求

  • 使用okhttp访问第三方数据API接口;

  • 使用gson库进行json数据解析及展示;

  • 使用多线程进行网络访问及数据处理;

实验内容

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

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

图1. 新闻列表页

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

1. 注册账号并获取APIKEY

2. 测试新闻API接口

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

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

>

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

>

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

    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对象数组,为实际返回的数据,需要进行解析;

不同接口返回的json对象结构不同,具体看接口的文档描述

    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"
        },
        ...
        ]
    }

步骤二,新建名为GGNews的项目

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

1. 添加第三方库依赖

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

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'`
    ...
  }
  • okhttp库版本:3.10.0;

  • gson库版本:2.7;

  • glide库版本:4.7.1;

2. 配置AndroidManifest.xml文件

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

<?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配置。

<?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布局如下代码所示。

<?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布局

<?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新闻类实例(这一过程称为解序列化)。

将一个Java对象转化为json对象字符串的过程称为序列化。

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

{
  "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对象字符串的属性名不同,可通过@SerializedName注解进行指定;此外,还可通过@Expose注解指定对应的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控件中进行显示,这步操作已经在其他项目中介绍过多次,在此不进行赘述,具体如下代码所示。

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等。

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类,则不利于代码维护。

通过将BaseResponse类定义为模板类,当接口A需要的是json数组,则在实例化BaseResponse时指定其模板类型为List即可,当接口B需要的是json对象时,则实例化BaseResponse时指定其模板类型为指定的某一Java类即可(例如News类)。

  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()方法中调用initView()私有方法初始化其布局,主要进行ListView视图控件绑定以及适配器绑定等操作,具体代码如下所示。

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()方法中,完成布局初始化后,直接使用okhttp请求API接口数据。 首先定义名为initData()的方法,在该方法中将初始化List列表对象用于保存从网络获取的新闻列表信息。其次,调用 refreshData()方法进行实际的API请求,具体代码如下所示。

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()方法中创建了子线程,并在子线程中进行网络请求。进行网络请求需要使用okhttp网络请求库,其核心是实例化OkHttpClient对象,并调用该对象的newCall()及enqueue()**方法进行异步请求。

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

如果需要使用POST方式向API接口请求数据,则可调用post()方法设置RequestBody请求体。

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

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

  • onResponse(Call call, Response response),当http请求有响应时被调用,通过Response对象的isSuccessful()方法可判断请求是否成功;

  • onFailure(Call call, IOException e),当http请求失败时被调用,可通过Call对象重新进行请求或取消请求,通过IOException对象可获取出错的原因。

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

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

runOnUiThread()方法接收一个Runnable接口对象作为参数,该接口中的run()方法将在UI主线程中被执行。

此时的okhttp3.Callback对象的onResponse()、onFailure()两个方法在refreshData()方法所开启的子线程中被执行。对于UI视图组件对象的操作必须在UI主线程中。

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

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

3. 定义NewsRequest新闻请求类

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

在实际应用开发中你可能需要定义一个请求类的基类,并从该基类派生出与每一个API对应的请求类。

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

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()方法,便于将其属性转换为API接口所需的参数。

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

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

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

实验小结

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

  • 使用Thread、Runnable启动子线程;

  • 使用okhttp进行网络请求;

  • 使用Gson进行Java对象序列化及解序列化;

图2. 新闻详情页

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

图3. 天行数据官网首页
图4. 天行数据控制台

返回天行数据官网首页,点击【接口】菜单进入API可用接口列表,选择【综合新闻】接口,进入该接口描述介绍页面, 如所示。接口描述页面包含对该接口使用、价格、请求参数、返回示例以及参考代码等介绍,可以快速帮助开发者熟悉接口的使用方法。

例如中,进行的HTTP请求地址及信息分别是:

请求地址: 请求方式: GET 请求参数: key:注册后申请的APIKEY num:接口分页时每页包含的新闻数量 page:当前页数

图5. 综合新闻API接口文档页
图6. 综合新闻API在线测试工具

Android Studio会检测所有*.gradle文件内容,一旦其发生修改便会在IDE中提醒用户同步项目依赖库, 如所示,直接点击Sync Now即可。 不同第三方库对于Android SDK版本依赖不一样。如何确定与你所选择的Android SDK对应的第三方库的 版本,可查看第三方库的官方文档。本项目中,使用的Android SDK版本为28,而第三方库的版本分别如下:

图7. 同步项目依赖库

要实现如所示的新闻列表页效果,需要定义ListView控件的Item布局,这里定义其布局文件名为list_item.xml,每一项Item需要显示标题、新闻来源、日期、标题图等信息,根据需要选择相应的视图控件,具体如下代码所示。

https://www.tianapi.com
http://api.tianapi.com/generalnews/
http://api.tianapi.com/generalnews/?page=1&num=10&key=注册的APIKEY
图5. 综合新闻API接口文档页
图6. 综合新闻API在线测试工具
图7. 同步项目依赖库
图1. 新闻列表页
第七个项目
第九个项目
图1. 新闻列表页