第二十五章 HTTP与后台任务
这几章需要科学上网,但是AVD似乎并不是一个合格的魔法师,所以只能在自己的设备上调试,很麻烦。
有关网络连接基本
第一件事是获取网络使用权限,在AndroidManifest文件中添加权限代码:
<uses-permission android:name="android.permission.INTERNET"/>
网络连接基本流程是:根据传入的字符串封装成一个url对象,调用其openConnection方法创建一个指向其的连接对象,创建输入流读取网络数据。其中openConnection返回的是URLConnection的对象,需要转型为HttpURLConnection对象,并且这个对象虽然提供了一个连接,但是只有当你真正开始读取数据时(getInputStream),它才会真正连接到指定的URL地址,才会有反馈代码(getResponseCode)。
private 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));
}
有关AsyncTask创建后台线程
所有Android应用都是从主线程开始的,主线程处于一个无线循环的运行状态,一旦有来自系统或者用户的操作行为,主线程就会做出相应的反应。
有很多操作需要花上很长的时间,如果这些操作全都交给主线程来做,那个这个应用就会被卡死,所以提出了这些很费时的操作必须交给后台进程来做,例如Android会强制要求所有网络连接全部在后台进程中完成,否则会报错。另外也只有主进程能够修改UI,这是因为主进程和后台进程的生命周期不同导致,如果后台进程可以修改UI,可能此时主进程已经消失了,这会导致不可思议的错误,所有主线程也叫UI线程。
AsyncTask是一个使用后台进程的工具类,我们可以在该线程上调用其doInBackground方法来执行网络连接操作。
private class FetchItemsTask extends AsyncTask<Void,Void,List<GalleryItem>> { @Override protected List<GalleryItem> doInBackground(Void ...integer) { //doConnection } ... }
AsyncTask实例的启动方式是其execute()方法,可以在Fragment的onCreate周期中调用。
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
new FetchItemsTask().execute();
}
有关获取并解析JSON数据
我们需要从Flick中获取JSON数据。从Flick接收JSON是利用之前的getUrlString方法并将其封装为JSONObject:
public List<GalleryItem> fetchItems(Integer integer) { List<GalleryItem> items=new ArrayList<>(); try { String url= Uri.parse("https://api.flickr.com/services/rest/") .buildUpon() .appendQueryParameter("method","flickr.photos.getRecent") .appendQueryParameter("api_key",API_KEY) .appendQueryParameter("format","json") .appendQueryParameter("nojsoncallback","1") .appendQueryParameter("extras","url_s") .appendQueryParameter("page",integer.toString()) .build() .toString(); String jsonString=getUrlString(url); Log.i(TAG,"Received JSON: "+jsonString); JSONObject jsonBody=new JSONObject(jsonString); ... } catch (IOException ioe) { ... } catch (JSONException e) { ... } return items; }
这里用的是Uri.Builder创建正确转义参数化的URL。
解析JSON数据
JSON对象(JSONObject)是一系列包含在{}中的名值对,JSON数组(JSONArray)是包含在[]中用逗号隔开的JSON对象列表,对象彼此嵌套形成层级关系。
使用getJSONObject和getJSONArray方法可以获取JSON对象的子数组或子对象:
private void parseItems(List<GalleryItem>items,JSONObject jsonbody) throws IOException,JSONException { JSONObject photosJsonObject=jsonbody.getJSONObject("photos"); JSONArray photoJsonArray=photosJsonObject.getJSONArray("photo"); ... }
获取到包含每一个图片信息的JSON对象后,可以用getString来获取相应的信息:
private void parseItems(List<GalleryItem>items,JSONObject jsonbody) throws IOException,JSONException { 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中更新数据与UI
AsyncTask提供了一个可覆盖方法onPostExecute来执行这些操作。onPostExecute保证在doInBackground方法完全执行完毕后,且在主进程中被执行。
private class FetchItemsTask extends AsyncTask<Void,Void,List<GalleryItem>> { ... @Override protected void onPostExecute(List<GalleryItem> galleryItems) { mItems.addAll(galleryItems); setupAdapter(); mTextView.setText(text1); } }
有关AsyncTask的三个类型参数
有关挑战练习 Gson
Gson是直接将JSON转化为Java类的有关工具库,所以要先定义一个Java类来存储数据:
public class PhotoResult {
private String stat;
private Photos photos;
public Photos getPhotos() {
return photos;
}
public void setPhotos(Photos photos) {
this.photos = photos;
}
public String getStat() {
return stat;
}
public void setStat(String stat) {
this.stat = stat;
}
public static class Photos{
private int page;
private int pages;
private int perpage;
private int total;
private List<detail> photo;
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getPages() {
return pages;
}
public void setPages(int pages) {
this.pages = pages;
}
public int getPerpage() {
return perpage;
}
public void setPerpage(int perpage) {
this.perpage = perpage;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public List<detail> getPhoto() {
return photo;
}
public void setPhoto(List<detail> photo) {
this.photo = photo;
}
public static class detail{
private String id;
private String owner;
private String secret;
private String server;
private String farm;
private String title;
private String url_s;
private int ispublic;
private int isfriend;
private int isfamily;
public String getUrl_s() {
return url_s;
}
public void setUrl_s(String url_s) {
this.url_s = url_s;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getServer() {
return server;
}
public void setServer(String server) {
this.server = server;
}
public String getFarm() {
return farm;
}
public void setFarm(String farm) {
this.farm = farm;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getIspublic() {
return ispublic;
}
public void setIspublic(int ispublic) {
this.ispublic = ispublic;
}
public int getIsfriend() {
return isfriend;
}
public void setIsfriend(int isfriend) {
this.isfriend = isfriend;
}
public int getIsfamily() {
return isfamily;
}
public void setIsfamily(int isfamily) {
this.isfamily = isfamily;
}
}
}
}
然后将接受的JSON数据直接用Gson映射就行了:
private void parseItems(List<GalleryItem>items,JSONObject jsonbody) throws IOException,JSONException { Gson gson = new Gson(); PhotoResult result = gson.fromJson(String.valueOf(jsonbody), PhotoResult.class); List<PhotoResult.Photos.detail> photo= result.getPhotos().getPhoto(); for (PhotoResult.Photos.detail ph:photo) { GalleryItem item=new GalleryItem(); item.setID(ph.getId()); item.setCaption(ph.getTitle()); if(ph.getUrl_s()==null) { continue; } item.setUrl(ph.getUrl_s()); items.add(item); } }
有关挑战练习 分页
首先要先知道如何监听RecyclerView是否滑动到最底部。
定义有关类来继承 RecyclerView.OnScrollListener 和实现 回调接口BottomListener:
public interface BottomListener {
void onScrollToBottom();
}
public class RecyclerViewScrollListener extends RecyclerView.OnScrollListener implements BottomListener {
// 最后几个完全可见项的位置(瀑布式布局会出现这种情况)
private int[] lastCompletelyVisiblePositions;
// 最后一个完全可见项的位置
private int lastCompletelyVisibleItemPosition;
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
// 找到最后一个完全可见项的位置
if (layoutManager instanceof GridLayoutManager) {
lastCompletelyVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
} else {
throw new RuntimeException("Unsupported LayoutManager.");
}
}
private int getMaxPosition(int[] positions) {
int max = positions[0];
for (int i = 1; i < positions.length; i++) {
if (positions[i] > max) {
max = positions[i];
}
}
return max;
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
// 通过比对 最后完全可见项位置 和 总条目数,来判断是否滑动到底部
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (visibleItemCount > 0 && lastCompletelyVisibleItemPosition >= totalItemCount - 1) {
onScrollToBottom();
}
}
}
@Override
public void onScrollToBottom() {
}
}
为RecyclerView设置监听器:
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { ... mPhotoRecyclerView.addOnScrollListener(new RecyclerViewScrollListener() { @Override public void onScrollToBottom() { // 加载更多 doLoadMore(); } }); } private void doLoadMore() { new FetchItemsTask().execute(); }
现在要完成的是如何让Flickr返回更多页的数据,先在Uri中添加page参数:
public List<GalleryItem> fetchItems(Integer integer) { List<GalleryItem> items=new ArrayList<>(); try { String url= Uri.parse("https://api.flickr.com/services/rest/") .buildUpon() .appendQueryParameter("method","flickr.photos.getRecent") .appendQueryParameter("api_key",API_KEY) .appendQueryParameter("format","json") .appendQueryParameter("nojsoncallback","1") .appendQueryParameter("extras","url_s") .appendQueryParameter("page",integer.toString()) .build() .toString(); ... } ... }
page的值是需要手动传进去的,传进去的页数在FetchItemsTask的doInBackground方法中决定,为了实现将后续结果添加到当前页后面,还需要修改item的更新方式(原来是直接赋值,现在是合并):
private class FetchItemsTask extends AsyncTask<Void,Void,List<GalleryItem>> { @Override protected List<GalleryItem> doInBackground(Void ...integer) { return new FlickrFetchr().fetchItems(nowPage++); } @Override protected void onPostExecute(List<GalleryItem> galleryItems) { mItems.addAll(galleryItems); setupAdapter(); ... } }
效果如下: