欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-01-04 16:12:16
...

本章主要讲了如何使用 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);
    }
});

GitHub Page: kniost.github.io
简书:http://www.jianshu.com/u/723da691aa42

上一篇:

下一篇: