本章主要講了如何使用 android 系統(tǒng)的網(wǎng)絡(luò)連接太惠,并介紹了格式化 JSON 和多線程編程 AsyncTask 的使用蓝角。另外,挑戰(zhàn)練習(xí)里還結(jié)合了 Gson 庫的使用罢屈。
GitHub 地址:
完成23章但未完成挑戰(zhàn)
完成23章挑戰(zhàn)1:使用 Gson
完成23章挑戰(zhàn)2:添加分頁
完成23章挑戰(zhàn)3:動態(tài)調(diào)整網(wǎng)格列
1. 網(wǎng)絡(luò)連接基本
首先要在 Manifest 文件中請求網(wǎng)絡(luò)權(quán)限
<uses-permission android:name="android.permission.INTERNET" />
然后我們建立一個網(wǎng)絡(luò)請求的函數(shù):
// FlickrFetchr.java
// 參數(shù)是 url 字符串,并且需要拋出 IO 錯誤
public byte[] getUrlBytes(String urlSpec) throws IOException {
URL url = new URL(urlSpec);
// 打開連接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
// 建立兩個流對象
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 使用 getInputStream() 方法時才會真正發(fā)送 GET 請求
// 如果要使用 POST 請求牍帚,需要調(diào)用 getOutputStream()
InputStream in = connection.getInputStream();
// 如果連接失敗就拋出錯誤
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException(connection.getResponseMessage() +
": with" +
urlSpec);
}
// 建立一個計數(shù)器
int bytesRead = 0;
// 建立一個緩存 buffer
byte[] buffer = new byte[1024];
// 用 InputStream.read 將數(shù)據(jù)讀取到 buffer 中儡遮,
// 然后寫到 OutputStream 中
while ((bytesRead = in.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
}
// 之后一定要關(guān)閉 OutputStream
out.close();
return out.toByteArray();
} finally {
// 最后要關(guān)閉連接
connection.disconnect();
}
}
public String getUrlString(String urlSpec) throws IOException {
// 將結(jié)果轉(zhuǎn)換成 String
return new String(getUrlBytes(urlSpec));
}
2. 線程與主線程
網(wǎng)絡(luò)連接需要時間,Web 服務(wù)器可能需要1~2秒的時間來響應(yīng)訪問請求暗赶,文件下載則耗時更久鄙币。考慮到這個因素蹂随,Android 禁止任何主線程網(wǎng)絡(luò)連接行為十嘿。即使強(qiáng)行在主線程中進(jìn)行網(wǎng)絡(luò)連接,Android 也會拋出 NetworkOnMainThreadException 異常岳锁。
這是為什么呢?要想知道绩衷,首先要了解什么是線程,什么是主線程以及主線程的用途是什么激率。
線程是個單一執(zhí)行序列咳燕。單個線程中的代碼會逐步執(zhí)行。所有 Android 應(yīng)用的運行都是從主線程開始的乒躺。然而招盲,主線程不是線程那樣的預(yù)定執(zhí)行序列。相反嘉冒,它處于一個無限循環(huán)的運行狀態(tài)曹货,等待著用戶或系統(tǒng)觸發(fā)事件的發(fā)生。事件觸發(fā)后讳推,主線程便負(fù)責(zé)執(zhí)行代碼顶籽,以響應(yīng)這些事件。
主線程運行著所有更新 UI 的代碼银觅,其中包括響應(yīng) activity 的啟動礼饱、按鈕的點擊等不同 UI 相關(guān)事件的代碼。(由于響應(yīng)的事件基本都與用戶界面相關(guān)究驴,主線程有時也叫作 UI 線程慨仿。)
事件處理循環(huán)讓 UI 代碼得以按順序執(zhí)行。這可以保證任何事件處理都不會發(fā)生沖突纳胧,同時代碼也能夠快速響應(yīng)執(zhí)行镰吆。
而網(wǎng)絡(luò)連接相比其他任務(wù)更耗時。等待響應(yīng)期間跑慕,用戶界面毫無反應(yīng)万皿,這可能會導(dǎo)致應(yīng)用無響應(yīng)(Application Not Responding,ANR)現(xiàn)象發(fā)生摧找,也就是一個彈框,要求你關(guān)閉應(yīng)用牢硅。
怎樣使用后臺線程最容易呢蹬耘?答案就是使用 AsyncTask 類
3. AsyncTask
3.1 AsyncTask 的生命
AsyncTask 類可以重寫的方法和一個進(jìn)程的生命過程對應(yīng):
-
onPreExecute()
執(zhí)行之前 -
onProgressUpdate()
更新進(jìn)展 -
doInBackground()
在線程中真正要完成的事 -
onPostExecute()
完成之后要做的事(在 UI 線程中執(zhí)行) -
onCancelled()
退出之后
3.2 AsyncTask 的三個參數(shù)
其中模板的三個類類型參數(shù)(不能是基礎(chǔ)類型)分別是:輸入、進(jìn)度减余、結(jié)果综苔。
3.2.1 第一個參數(shù):輸入
第一個類型參數(shù)可指定輸入?yún)?shù)的類型∥徊恚可參考以下示例使用該參數(shù):
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;
}
};
輸入?yún)?shù)傳入 execute(...)方法(可接受一個或多個參數(shù)): task.execute("第一個參數(shù)", "第二個參數(shù)", "……");
然后如筛,再把這些變量參數(shù)傳遞給 doInBackground(...)方法。
3.2.2 第二個參數(shù):進(jìn)度
第二個類型參數(shù)可指定發(fā)送進(jìn)度更新需要的類型抒抬。以下為示例代碼:
final ProgressBar gestationProgressBar = /* 一個特定的進(jìn)度條 */;
gestationProgressBar.setMax(42); /* 最大的進(jìn)度 */
AsyncTask<Void,Integer,Void> haveABaby = new AsyncTask<Void,Integer,Void>() {
public Void doInBackground(Void... params) {
while (!babyIsBorn()) {
Integer weeksPassed = getNumberOfWeeksPassed();
publishProgress(weeksPassed); // 關(guān)鍵杨刨,將參數(shù)發(fā)送到 onProgressUpdate
patientlyWaitForBaby();
}
}
public void onProgressUpdate(Integer... params) {
int progress = params[0];
gestationProgressBar.setProgress(progress);
}
};
/* call when you want to execute the AsyncTask */
haveABaby.execute();
進(jìn)度更新通常發(fā)生在執(zhí)行的后臺進(jìn)程中。問題是擦剑,在后臺進(jìn)程中無法完成必要的 UI 更新妖胀。因此 AsyncTask 提供了 publishProgress(...)和 onProgressUpdate(...)方法。
其工作方式是這樣的 : 在后臺線程中 , 從 doInBackground(...) 方法中調(diào)用 publishProgress(...)方法惠勒。這樣 onProgressUpdate(...)方法便能夠在 UI 線程上調(diào)用赚抡。因此,在 onProgressUpdate(...)方法中執(zhí)行 UI 更新就可行了纠屋,但必須在 doInBackground(...) 方法中使用 publishProgress(...)方法對它們進(jìn)行管控涂臣。
3.2.3 第三個參數(shù):結(jié)果
第三個類型參數(shù)是處理結(jié)果返回的類型參數(shù)。下面是本章的示例代碼
// PhotoGalleryFragment.java
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();
}
}
第三個參數(shù)就是在 doInBackground 中返回的結(jié)果巾遭,我們需要從后臺請求 API 返回的 JSON 數(shù)據(jù)肉康,然后將其格式化闯估,返回的就是我們需要的數(shù)據(jù)灼舍。
4. JSON 數(shù)據(jù)解析
什么是 JSON 數(shù)據(jù)呢?JSON(JavaScript Object Notation) 是一種輕量級的數(shù)據(jù)交換格式涨薪。易于人閱讀和編寫骑素。同時也易于機(jī)器解析和生成。它基于 JavaScript 的一個子集刚夺。JSON 采用完全獨立于語言的文本格式献丑,但是也使用了類似于C語言家族的習(xí)慣(包括C, C++, C#, Java, JavaScript, Perl, Python 等)。這些特性使 JSON 成為理想的數(shù)據(jù)交換語言侠姑。
JSON 對象是一系列包含在{ }中的名值對创橄。JSON 數(shù)組是包含在[ ]中用逗號隔開的 JSON 對象列表。對象彼此嵌套形成層級關(guān)系莽红。詳細(xì)的語法可以查看JSON 官網(wǎng)妥畏。
JSON 這種數(shù)據(jù)格式在同樣基于這些結(jié)構(gòu)的編程語言之間交換十分方便邦邦,所以網(wǎng)絡(luò)服務(wù)器端越來越多地開始用 JSON 來交換數(shù)據(jù),我們在這章使用的 API 同樣如此醉蚁。
一個例子
// 為節(jié)省版面燃辖,去掉了無關(guān)的屬性
{
"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"
}
對應(yīng)的解析代碼:
// 解析時用 try…catch,要拋出 JSONException 防止程序崩潰
// JSONObject 構(gòu)造方法解析傳入的 JSON 數(shù)據(jù)后
// 會生成與原始 JSON 數(shù)據(jù)對應(yīng)的對象樹
JSONObject jsonBody = new JSONObject(jsonString);
// 頂層 JSONObject 對應(yīng)著原始數(shù)據(jù)最外層的{ }网棍。它包含了一個叫作 photos 的嵌套 JSONObject
JSONObject photosJsonObject = jsonBody.getJSONObject("photos");
// 這個嵌套對象又包含了一個叫作 photo 的 JSONArray
JSONArray photoJsonArray = photosJsonObject.getJSONArray("photo");
// 這個嵌套數(shù)組中又包含了一組 JSONObject
// 這些 JSONObeject 就是要獲取的一張張圖片的元數(shù)據(jù)
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 進(jìn)行更新了黔龟。
5. 挑戰(zhàn)練習(xí)
本章的挑戰(zhàn)練習(xí)難度依次遞增,考驗了我們很多知識滥玷。
5.1 使用 Gson 庫解析 JSON 數(shù)據(jù)
Gson 是 Google 官方推薦的 JSON 解析庫氏身,使用 Gson 不用寫任何解析代碼,它能自動將 JSON 數(shù)據(jù)映射為 Java 對象罗捎。
5.1.1 添加 Gson 依賴
在 File -> Project Structure -> Dependencies 中添加 gson 依賴
5.1.2 構(gòu)建對應(yīng)的 POJO 類
由于不想更改原本的 GalleryItem 類观谦,并且想讓成員變量的命名符合 java 的命名規(guī)范,我使用了 @SerializedName()
注解桨菜,這個注解注明了 Gson 在轉(zhuǎn)換時對應(yīng)的鍵名豁状。并且構(gòu)建了一個新的類,用于匹配對應(yīng)的 API 結(jié)構(gòu):
// PhotoBean.java
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;
}
}
// 省略 getter 和 setter
}
5.1.3 使用 Gson
Gson 的使用再簡單不過了倒得,與上面的代碼相比有云泥之別:
PhotoBean photoBean = (PhotoBean) new Gson()
.fromJson(jsonString, PhotoBean.class);
不過記得要拋出 JsonSyntaxException泻红。
5.2 分頁顯示
這個挑戰(zhàn)的需求是:如果我們下滑最底部,就在后面添加下一頁的內(nèi)容霞掺。
所以在 url 的生成中我們還要加入 page 這個參數(shù)谊路。我加入了一個成員變量 mNextPage 用于記錄下次要請求的頁面, 然后添加了一個常量 MAX_PAGES 用于控制最大請求頁數(shù)菩彬。
5.2.1 RecyclerView.onScrollListener
onScrollListener 有兩個可以重寫的方法缠劝,一個是 onScrollStateChanged(),還有一個是 onScrolled骗灶,對我們這個需求來說惨恭,顯然 onScrollStateChanged 比較合適,ScrollState 也有三種:
-
SCROLL_STATE_IDLE
: 視圖沒有被拖動耙旦,處于靜止 -
SCROLL_STATE_DRAGGING
: 視圖正在拖動中 -
SCROLL_STATE_SETTLING
: 視圖在慣性滾動
這個挑戰(zhàn)最關(guān)鍵的就是如何判斷滑到最底端脱羡。首先滑動到最底端時前兩個狀態(tài)其實都可以,但是滑動到最底這個信息只有 LayoutManager 知道免都,我們可以直接看代碼分析:
private RecyclerView.OnScrollListener onButtomListener =
new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// 首先獲取 LayoutManager
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
// 然后可以找到最后顯示的位置锉罐,一旦滾動就會獲取該位置
mLastPosition = layoutManager.findLastCompletelyVisibleItemPosition();
// 如果靜止的時候最后的位置大于等于數(shù)據(jù)個數(shù)
// 而且前一個任務(wù)完成時(防止多次重復(fù))
if (newState == RecyclerView.SCROLL_STATE_IDLE
&& mLastPosition >= mPhotoAdapter.getItemCount() - 1) {
if (mFetchItemsTask.getStatus() == AsyncTask.Status.FINISHED) {
// 下一頁加一,在小于最大頁數(shù)時
// 彈出 Toast 表示正在加載
// 然后打開一個新任務(wù)绕娘,加載下一頁
mNextPage++;
if (mNextPage <= MAX_PAGES) {
Toast.makeText(getActivity(), "waiting to load ……", Toast.LENGTH_SHORT).show();
// AsyncTask 只能執(zhí)行一次脓规,所以需要新建
mFetchItemsTask = new FetchItemsTask();
mFetchItemsTask.execute(mNextPage);
} else {
// 滑到最底提示已經(jīng)到頭了
Toast.makeText(getActivity(), "This is the end!", Toast.LENGTH_SHORT).show();
}
}
}
}
};
5.2.2 添加數(shù)據(jù)并展示
我在 Adapter 中加入了一個 addData 方法,將新的數(shù)據(jù)加入到數(shù)據(jù)集中险领,然后使用 notifyDataSetChanged 方法更新視圖侨舆。
然后修改了 setAdapter 方法:
private void setAdapter() {
if (isAdded()) {
if (mPhotoAdapter == null) {
mPhotoAdapter = new PhotoAdapter(mItems);
mPhotoRecyclerView.setAdapter(mPhotoAdapter);
mPhotoRecyclerView.addOnScrollListener(onButtomListener);
} else {
mPhotoAdapter.addData(mItems);
}
}
}
5.3 動態(tài)調(diào)整網(wǎng)格列
使用 OnGlobalLayoutListener 即可:
mPhotoRecyclerView.getViewTreeObserver()
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 計算列數(shù)升酣,以 1080p 屏幕顯示3列為基準(zhǔn)
int columns = mPhotoRecyclerView.getWidth() / 350;
// 重新設(shè)置 LayoutManager、Adapter 和 Listener
mPhotoRecyclerView.setLayoutManager(new GridLayoutManager(getActivity(), columns));
mPhotoRecyclerView.setAdapter(mPhotoAdapter);
mPhotoRecyclerView.addOnScrollListener(onButtomListener);
// 滾動到之前看到的位置
mPhotoRecyclerView.getLayoutManager().scrollToPosition(mLastPosition);
//將 GlobalLayoutListener 去掉以避免多次觸發(fā)
mPhotoRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
GitHub Page: kniost.github.io
簡書:http://www.reibang.com/u/723da691aa42