Android编程权威指南(第二版)学习笔记(二十三)—— 第23章 HTTP 与后台任务

本章主要讲了如何使用 android 系统的网络连接,并介绍了格式化 JSON 和多线程编程 AsyncTask 的使用。另外,挑战练习里还结合了 Gson 库的使用。

GitHub 地址:
完成23章但未完成挑战
完成23章挑战1:使用 Gson
完成23章挑战2:添加分页
完成23章挑战3:动态调整网格列

1. 网络连接基本

首先要在 Manifest 文件中请求网络权限

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

然后我们建立一个网络请求的函数:

// FlickrFetchr.java
// 参数是 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() 方法时才会真正发送 GET 请求
        // 如果要使用 POST 请求,需要调用 getOutputStream()
        InputStream in = connection.getInputStream();
        // 如果连接失败就抛出错误
        if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
            throw new IOException(connection.getResponseMessage() +
                    ": with" +
                    urlSpec);
        }

        // 建立一个计数器
        int bytesRead = 0;
        // 建立一个缓存 buffer
        byte[] buffer = new byte[1024];
        // 用 InputStream.read 将数据读取到 buffer 中,
        // 然后写到 OutputStream 中
        while ((bytesRead = in.read(buffer)) > 0) {
            out.write(buffer, 0, bytesRead);
        }
        // 之后一定要关闭 OutputStream
        out.close();
        return out.toByteArray();
    } finally {
        // 最后要关闭连接
        connection.disconnect();
    }
}

public String getUrlString(String urlSpec) throws IOException {
    // 将结果转换成 String
    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 第一个参数:输入

第一个类型参数可指定输入参数的类型。可参考以下示例使用该参数:

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 第二个参数:进度

第二个类型参数可指定发送进度更新需要的类型。以下为示例代码:

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); // 关键,将参数发送到 onProgressUpdate
          patientlyWaitForBaby();
        } 
    }

    public void onProgressUpdate(Integer... params) {
        int progress = params[0];
        gestationProgressBar.setProgress(progress);
    } 
};
/* call when you want to execute the AsyncTask */
haveABaby.execute();

进度更新通常发生在执行的后台进程中。问题是,在后台进程中无法完成必要的 UI 更新。因此 AsyncTask 提供了 publishProgress(…)和 onProgressUpdate(…)方法。
其工作方式是这样的 : 在后台线程中 , 从 doInBackground(…) 方法中调用 publishProgress(…)方法。这样 onProgressUpdate(…)方法便能够在 UI 线程上调用。因此,在 onProgressUpdate(…)方法中执行 UI 更新就可行了,但必须在 doInBackground(…) 方法中使用 publishProgress(…)方法对它们进行管控。

3.2.3 第三个参数:结果

第三个类型参数是处理结果返回的类型参数。下面是本章的示例代码

// 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();
    }
}

第三个参数就是在 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 同样如此。

一个例子

// 为节省版面,去掉了无关的属性
{
  "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"
}

对应的解析代码:

// 解析时用 try…catch,要抛出 JSONException 防止程序崩溃
// JSONObject 构造方法解析传入的 JSON 数据后
// 会生成与原始 JSON 数据对应的对象树
JSONObject jsonBody = new JSONObject(jsonString);

// 顶层 JSONObject 对应着原始数据最外层的{ }。它包含了一个叫作 photos 的嵌套 JSONObject
JSONObject photosJsonObject = jsonBody.getJSONObject("photos");

// 这个嵌套对象又包含了一个叫作 photo 的 JSONArray
JSONArray photoJsonArray = photosJsonObject.getJSONArray("photo");

// 这个嵌套数组中又包含了一组 JSONObject
// 这些 JSONObeject 就是要获取的一张张图片的元数据
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 结构:

// 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 分页显示

这个挑战的需求是:如果我们下滑最底部,就在后面添加下一页的内容。
所以在 url 的生成中我们还要加入 page 这个参数。我加入了一个成员变量 mNextPage 用于记录下次要请求的页面, 然后添加了一个常量 MAX_PAGES 用于控制最大请求页数。

5.2.1 RecyclerView.onScrollListener

onScrollListener 有两个可以重写的方法,一个是 onScrollStateChanged(),还有一个是 onScrolled,对我们这个需求来说,显然 onScrollStateChanged 比较合适,ScrollState 也有三种:

  • SCROLL_STATE_IDLE: 视图没有被拖动,处于静止
  • SCROLL_STATE_DRAGGING: 视图正在拖动中
  • SCROLL_STATE_SETTLING: 视图在惯性滚动

这个挑战最关键的就是如何判断滑到最底端。首先滑动到最底端时前两个状态其实都可以,但是滑动到最底这个信息只有 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();
        // 如果静止的时候最后的位置大于等于数据个数
        // 而且前一个任务完成时(防止多次重复)
        if (newState == RecyclerView.SCROLL_STATE_IDLE
                && mLastPosition >= mPhotoAdapter.getItemCount() - 1) {
            if (mFetchItemsTask.getStatus() == AsyncTask.Status.FINISHED) {
                // 下一页加一,在小于最大页数时
                // 弹出 Toast 表示正在加载
                // 然后打开一个新任务,加载下一页
                mNextPage++;
                if (mNextPage <= MAX_PAGES) {
                    Toast.makeText(getActivity(), "waiting to load ……", Toast.LENGTH_SHORT).show();
                    // AsyncTask 只能执行一次,所以需要新建
                    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 方法:

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 即可:

mPhotoRecyclerView.getViewTreeObserver()
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        // 计算列数,以 1080p 屏幕显示3列为基准
        int columns = mPhotoRecyclerView.getWidth() / 350;
        // 重新设置 LayoutManager、Adapter 和 Listener
        mPhotoRecyclerView.setLayoutManager(new GridLayoutManager(getActivity(), columns));
        mPhotoRecyclerView.setAdapter(mPhotoAdapter);
        mPhotoRecyclerView.addOnScrollListener(onButtomListener);
        // 滚动到之前看到的位置
        mPhotoRecyclerView.getLayoutManager().scrollToPosition(mLastPosition);
        //将 GlobalLayoutListener 去掉以避免多次触发
        mPhotoRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
    }
});
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值