本章主要讲了如何使用 android 系统的网络连接,并介绍了格式化 JSON 和多线程编程 AsyncTask 的使用。另外,挑战练习里还结合了 Gson 库的使用。
GitHub 地址:
完成23章但未完成挑战
完成23章挑战1:使用 Gson
完成23章挑战2:添加分页
完成23章挑战3:动态调整网格列
1. 网络连接基本
首先要在 Manifest 文件中请求网络权限
1
| <uses-permission android:name="android.permission.INTERNET" />
|
然后我们建立一个网络请求的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
public byte[] getUrlBytes(String urlSpec) throws IOException { URL url = new URL(urlSpec); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try { ByteArrayOutputStream out = new ByteArrayOutputStream(); InputStream in = connection.getInputStream(); if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { throw new IOException(connection.getResponseMessage() + ": with" + urlSpec); } int bytesRead = 0; byte[] buffer = new byte[1024]; while ((bytesRead = in.read(buffer)) > 0) { out.write(buffer, 0, bytesRead); } out.close(); return out.toByteArray(); } finally { connection.disconnect(); } }
public String getUrlString(String urlSpec) throws IOException { return new String(getUrlBytes(urlSpec)); }
|
2. 线程与主线程
网络连接需要时间,Web 服务器可能需要1~2秒的时间来响应访问请求,文件下载则耗时更久。考虑到这个因素,Android 禁止任何主线程网络连接行为。即使强行在主线程中进行网络连接,Android 也会抛出 NetworkOnMainThreadException 异常。
这是为什么呢?要想知道,首先要了解什么是线程,什么是主线程以及主线程的用途是什么。
线程是个单一执行序列。单个线程中的代码会逐步执行。所有 Android 应用的运行都是从主线程开始的。然而,主线程不是线程那样的预定执行序列。相反,它处于一个无限循环的运行状态,等待着用户或系统触发事件的发生。事件触发后,主线程便负责执行代码,以响应这些事件。
主线程运行着所有更新 UI 的代码,其中包括响应 activity 的启动、按钮的点击等不同 UI 相关事件的代码。(由于响应的事件基本都与用户界面相关,主线程有时也叫作 UI 线程。)
事件处理循环让 UI 代码得以按顺序执行。这可以保证任何事件处理都不会发生冲突,同时代码也能够快速响应执行。
而网络连接相比其他任务更耗时。等待响应期间,用户界面毫无反应,这可能会导致应用无响应(Application Not Responding,ANR)现象发生,也就是一个弹框,要求你关闭应用。
怎样使用后台线程最容易呢?答案就是使用 AsyncTask 类
3. AsyncTask
3.1 AsyncTask 的生命
AsyncTask 类可以重写的方法和一个进程的生命过程对应:
onPreExecute()
执行之前
onProgressUpdate()
更新进展
doInBackground()
在线程中真正要完成的事
onPostExecute()
完成之后要做的事(在 UI 线程中执行)
onCancelled()
退出之后
3.2 AsyncTask 的三个参数
其中模板的三个类类型参数(不能是基础类型)分别是:输入、进度、结果。
3.2.1 第一个参数:输入
第一个类型参数可指定输入参数的类型。可参考以下示例使用该参数:
1 2 3 4 5 6 7 8
| AsyncTask<String,Void,Void> task = new AsyncTask<String,Void,Void>() { public Void doInBackground(String... params) { for (String parameter : params) { Log.i(TAG, "Received parameter: " + parameter); } return null; } };
|
输入参数传入 execute(…)方法(可接受一个或多个参数): task.execute(“第一个参数”, “第二个参数”, “……”);
然后,再把这些变量参数传递给 doInBackground(…)方法。
3.2.2 第二个参数:进度
第二个类型参数可指定发送进度更新需要的类型。以下为示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| final ProgressBar gestationProgressBar = ; gestationProgressBar.setMax(42); AsyncTask<Void,Integer,Void> haveABaby = new AsyncTask<Void,Integer,Void>() { public Void doInBackground(Void... params) { while (!babyIsBorn()) { Integer weeksPassed = getNumberOfWeeksPassed(); publishProgress(weeksPassed); patientlyWaitForBaby(); } } public void onProgressUpdate(Integer... params) { int progress = params[0]; gestationProgressBar.setProgress(progress); } };
haveABaby.execute();
|
进度更新通常发生在执行的后台进程中。问题是,在后台进程中无法完成必要的 UI 更新。因此 AsyncTask 提供了 publishProgress(…)和 onProgressUpdate(…)方法。
其工作方式是这样的 : 在后台线程中 , 从 doInBackground(…) 方法中调用 publishProgress(…)方法。这样 onProgressUpdate(…)方法便能够在 UI 线程上调用。因此,在 onProgressUpdate(…)方法中执行 UI 更新就可行了,但必须在 doInBackground(…) 方法中使用 publishProgress(…)方法对它们进行管控。
3.2.3 第三个参数:结果
第三个类型参数是处理结果返回的类型参数。下面是本章的示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
private class FetchItemsTask extends AsyncTask<Integer, Void, List<GalleryItem>> { @Override protected List<GalleryItem> doInBackground(Integer... params) { return new FlickrFetchr().fetchItems(params[0]); }
@Override protected void onPostExecute(List<GalleryItem> galleryItems) { mItems = galleryItems; setAdapter(); } }
|
第三个参数就是在 doInBackground 中返回的结果,我们需要从后台请求 API 返回的 JSON 数据,然后将其格式化,返回的就是我们需要的数据。
4. JSON 数据解析
什么是 JSON 数据呢?JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于 JavaScript 的一个子集。JSON 采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python 等)。这些特性使 JSON 成为理想的数据交换语言。
JSON 对象是一系列包含在{ }中的名值对。JSON 数组是包含在[ ]中用逗号隔开的 JSON 对象列表。对象彼此嵌套形成层级关系。详细的语法可以查看JSON 官网。
JSON 这种数据格式在同样基于这些结构的编程语言之间交换十分方便,所以网络服务器端越来越多地开始用 JSON 来交换数据,我们在这章使用的 API 同样如此。
一个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| { "photos": { "page": 1, "pages": 10, "photo": [ { "id": "31987348504", "title": "Penny", "url_s": "https://farm3.staticflickr.com/2915/31987348504_9a949c482d_m.jpg", }, { "id": "31987352214", "title": "", "url_s": "https://farm1.staticflickr.com/455/31987352214_58428f3a9d_m.jpg", } ] }, "stat": "ok" }
|
对应的解析代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
JSONObject jsonBody = new JSONObject(jsonString);
JSONObject photosJsonObject = jsonBody.getJSONObject("photos");
JSONArray photoJsonArray = photosJsonObject.getJSONArray("photo");
for (int i = 0; i < photoJsonArray.length(); i++) { JSONObject photoJsonObject = photoJsonArray.getJSONObject(i); GalleryItem item = new GalleryItem(); item.setId(photoJsonObject.getString("id")); item.setCaption(photoJsonObject.getString("title")); if (!photoJsonObject.has("url_s")) { continue; } item.setUrl(photoJsonObject.getString("url_s")); items.add(item); }
|
解析完成后就可以在 AsyncTask 的 onPostExecute 中对 UI 进行更新了。
5. 挑战练习
本章的挑战练习难度依次递增,考验了我们很多知识。
5.1 使用 Gson 库解析 JSON 数据
Gson 是 Google 官方推荐的 JSON 解析库,使用 Gson 不用写任何解析代码,它能自动将 JSON 数据映射为 Java 对象。
5.1.1 添加 Gson 依赖
在 File -> Project Structure -> Dependencies 中添加 gson 依赖
5.1.2 构建对应的 POJO 类
由于不想更改原本的 GalleryItem 类,并且想让成员变量的命名符合 java 的命名规范,我使用了 @SerializedName()
注解,这个注解注明了 Gson 在转换时对应的键名。并且构建了一个新的类,用于匹配对应的 API 结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
public class PhotoBean {
public static final String STATUS_OK = "ok" , STATUS_FAILED = "fail";
@SerializedName("photos") private PhotosInfo mPhotoInfo; @SerializedName("stat") private String mStatus; @SerializedName("message") private String mMessage;
public class PhotosInfo { @SerializedName("photo") List<GalleryItem> mPhoto;
public List<GalleryItem> getPhoto() { return mPhoto; } } }
|
5.1.3 使用 Gson
Gson 的使用再简单不过了,与上面的代码相比有云泥之别:
1 2
| PhotoBean photoBean = (PhotoBean) new Gson() .fromJson(jsonString, PhotoBean.class);
|
不过记得要抛出 JsonSyntaxException。
5.2 分页显示
这个挑战的需求是:如果我们下滑最底部,就在后面添加下一页的内容。
所以在 url 的生成中我们还要加入 page 这个参数。我加入了一个成员变量 mNextPage 用于记录下次要请求的页面, 然后添加了一个常量 MAX_PAGES 用于控制最大请求页数。
onScrollListener 有两个可以重写的方法,一个是 onScrollStateChanged(),还有一个是 onScrolled,对我们这个需求来说,显然 onScrollStateChanged 比较合适,ScrollState 也有三种:
SCROLL_STATE_IDLE
: 视图没有被拖动,处于静止
SCROLL_STATE_DRAGGING
: 视图正在拖动中
SCROLL_STATE_SETTLING
: 视图在惯性滚动
这个挑战最关键的就是如何判断滑到最底端。首先滑动到最底端时前两个状态其实都可以,但是滑动到最底这个信息只有 LayoutManager 知道,我们可以直接看代码分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| private RecyclerView.OnScrollListener onButtomListener = new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager(); mLastPosition = layoutManager.findLastCompletelyVisibleItemPosition(); if (newState == RecyclerView.SCROLL_STATE_IDLE && mLastPosition >= mPhotoAdapter.getItemCount() - 1) { if (mFetchItemsTask.getStatus() == AsyncTask.Status.FINISHED) { mNextPage++; if (mNextPage <= MAX_PAGES) { Toast.makeText(getActivity(), "waiting to load ……", Toast.LENGTH_SHORT).show(); mFetchItemsTask = new FetchItemsTask(); mFetchItemsTask.execute(mNextPage); } else { Toast.makeText(getActivity(), "This is the end!", Toast.LENGTH_SHORT).show(); } } } } };
|
5.2.2 添加数据并展示
我在 Adapter 中加入了一个 addData 方法,将新的数据加入到数据集中,然后使用 notifyDataSetChanged 方法更新视图。
然后修改了 setAdapter 方法:
1 2 3 4 5 6 7 8 9 10 11
| private void setAdapter() { if (isAdded()) { if (mPhotoAdapter == null) { mPhotoAdapter = new PhotoAdapter(mItems); mPhotoRecyclerView.setAdapter(mPhotoAdapter); mPhotoRecyclerView.addOnScrollListener(onButtomListener); } else { mPhotoAdapter.addData(mItems); } } }
|
5.3 动态调整网格列
使用 OnGlobalLayoutListener 即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| mPhotoRecyclerView.getViewTreeObserver() .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { int columns = mPhotoRecyclerView.getWidth() / 350; mPhotoRecyclerView.setLayoutManager(new GridLayoutManager(getActivity(), columns)); mPhotoRecyclerView.setAdapter(mPhotoAdapter); mPhotoRecyclerView.addOnScrollListener(onButtomListener); mPhotoRecyclerView.getLayoutManager().scrollToPosition(mLastPosition); mPhotoRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); } });
|
GitHub Page: kniost.github.io
简书:http://www.jianshu.com/u/723da691aa42