实验目的
掌握okhttp 网络库进行同步、异步网络请求的方法;
实验要求
实验内容
在第七个项目 及第九个项目 中使用ListView 实现新闻列表,这两个项目中的新闻列表数据分别从字符串资源文件arrays.xml 或SQLite 数据库进行读取。 在实际应用项目中,数据往往存储于云端服务器中,通过http restful 接口等形式进行获取,数据的格式包括json 、xml 等形式。通常的做法是每一类数据对应一个API 接口,App 通过http 请求后,获取响应的数据后进行解析并与相应的UI 控件进行数据绑定从而完成数据展示。
http restful 接口通常需要根据项目需求自行设计开发并部署至云端服务上,以便App 能够进行访问。在本项目中,我们使用开放第三方API 接口服务,这样省去了进行接口开发的环节,便于我们聚焦项目的功能要求。 目前有较多第三方API 接口服务商,提供不同类型的API 接口服务,例如:聚合数据、天行数据、极速数据等。本项目中使用天行数据提供的综合新闻API 获取新闻列表,并通过WebView 控件展示指定新闻的详情页,项目运行效果如图1. 新闻列表页 所示。
步骤一,注册天行数据账号获取APIKEY
1. 注册账号并获取APIKEY
通过Web浏览器打开天行数据官网https://www.tianapi.com ,点击页面右上角【立即注册】,填写注册信息后进入天行数据控制台在【账号中心】查看APIKEY 。APIKEY 是使用天行数据API 接口时需要附带的一个标识字符串,用于做接口鉴权,具体使用方式在后续步骤中进行介绍。
2. 测试新闻API接口
在本项目中需要使用到综合新闻的API 接口,首先通过天行数据提供的网页版接口测试工具熟悉接口的调用参数及json 数据返回格式。
返回天行数据官网首页,点击【接口】菜单进入API 可用接口列表,选择【综合新闻】接口,进入该接口描述介绍页面, 如图5. 综合新闻API接口文档页 所示。接口描述页面包含对该接口使用、价格、请求参数、返回示例以及参考代码等介绍,可以快速帮助开发者熟悉接口的使用方法。
此外,点击【在线测试】按钮可进入HTTP 请求在线测试工具页面,通过该页面可动态填写接口的请求地址、请求参数等,并在网页端查看测试的返回内容便于开发者直观的了解和使用接口。
例如图6. 综合新闻API在线测试工具 中,进行的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 中进行展示。
Copy 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对象结构不同,具体看接口的文档描述
Copy 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 (图片加载库)三个第三方库的依赖,代码如下所示。
Copy 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 会检测所有*.gradle 文件内容,一旦其发生修改便会在IDE 中提醒用户同步项目依赖库, 如图7. 同步项目依赖库 所示,直接点击Sync Now 即可。 不同第三方库对于Android SDK 版本依赖不一样。如何确定与你所选择的Android SDK 对应的第三方库的 版本,可查看第三方库的官方文档。本项目中,使用的Android SDK 版本为28,而第三方库的版本分别如下:
2. 配置AndroidManifest.xml文件
为GGNews 项目配置网络访问权限,打开AndroidManifest.xml 文件,新增权限声明,代码如下所示。
Copy <?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 配置。
Copy <?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 布局如下代码所示。
Copy <?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. 新闻列表页 所示的新闻列表页效果,需要定义ListView 控件的Item 布局,这里定义其布局文件名为list_item.xml ,每一项Item 需要显示标题、新闻来源、日期、标题图等信息,根据需要选择相应的视图控件,具体如下代码所示。
Copy <?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 对象所含的属性,此外还可包含其他额外属性。
Copy {
"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"
}
以下为例,一条新闻包含:
gson 库会将一个json 对象字符串默认按照属性名进行解序列化成指定的Java类对象(在本例中是News 类)。如果Java类中指定的属性名与json 对象字符串的属性名不同,可通过@SerializedName 注解进行指定;此外,还可通过@Expose 注解指定对应的Java类属性是否进行序列化或解序列化,具体如下代码所示。
Copy 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 控件中进行显示,这步操作已经在其他项目中介绍过多次,在此不进行赘述,具体如下代码所示。
Copy 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 等。
Copy 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 类)。
Copy 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 视图控件绑定以及适配器绑定等操作,具体代码如下所示。
Copy 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 请求,具体代码如下所示。
Copy 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 的请求类,具体代码如下所示。
Copy 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,必填;
NewsRequest 本质上是一个普通的Java 类,在此我们重载了其toString() 方法,便于将其属性转换为API 接口所需的参数。
由于在此例子中,我们使用http GET 方式请求API 接口,因此直接将NewsRequest 的属性通过toString() 方法拼接成参数字符串。 如果使用http POST 方法,并且指定请求体为json数据格式,则需要gson 等第三方解析库将NewsRequest 对象进行序列化。
步骤六,编译项目并部署应用
完成上述五个步骤后,通过Android Studio 编译本项目并将应用部署于虚拟机或物理机等测试设备上,App启动后 查看其是否在ListView 中正确显示所获取的新闻列表。
实验小结
通过本次实验,你应该掌握了如下知识内容: