Android

Java安卓学习总结(二十九)

第二十五章 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线程。

Android在子线程中更新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();
        ...
    }
}

效果如下:

ScreenRecord 2020 07 03 12 29 49

发表评论