Android基础回顾(八)| 使用HTTP协议访问网络

参考书籍:《第一行代码》 第二版 郭霖
如有错漏,请批评指出!

使用HTTP协议访问网络

  • 使用HttpURLConnection

    在Android6.0之后,HttpClient被完全移除,官方建议使用HttpURLConnection进行网络请求,下面先来说说如何使用:

    1. 首先创建一个URL对象:
    URL url = new URL("https://www.baidu.com")
    
    1. 调用URL的openConnection()方法获得一个HttpURLConnection对象:
    HttpURLConnection connection = (HttpURLConnection)url.openConnetion()
    
    1. 设置HTTP请求所使用的方法:(常用的方法一般为 GET 或 POST)
    connection.setRequestMathod("GET")
    
    1. 接下来可以进行一些自由的定制:(比如设置连接超时、读取超时的时长,或者服务器希望得到的消息头等。)
    //设置连接超时时长(单位:ms)
    connection.setConnecTimeout(8000);
    //设置读取超时时长(单位:ms)
    connection.setReadTmeout(8000);
    
    1. 然后调用getInputStream()方法可以获取到服务器返回的输入流,接下来就是对输入流进行处理。
    InputStream in = connection.getInputStream();
    
    1. 最后别忘了将这个HTTP连接关闭:
    connection.disconnect();
    

    接下来是一个示例,获取百度主页的内容,并显示出来。

    public class ThirdActivity extends BaseActivity implements View.OnClickListener {
    
        private TextView responseText;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_third);
            initViews();
        }
    
        private void initViews(){
            Button send_request = (Button)findViewById(R.id.send_request);
            send_request.setOnClickListener(this);
            responseText = (TextView)findViewById(R.id.text_response);
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()){
                case R.id.send_request:
                    sendRequestWithHttpURLConnection("https://www.baidu.com");
                    break;
                default:
                    break;
            }
        }
    
        private void sendRequestWithHttpURLConnection(final String link){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    HttpURLConnection connection = null;
                    BufferedReader bufferedReader = null;
                    try{
                        URL url = new URL(link);
                        connection = (HttpURLConnection) url.openConnection();
                        connection.setRequestMethod("GET");
                        connection.setConnectTimeout(8000);
                        connection.setReadTimeout(8000);
                        InputStream in = connection.getInputStream();
                        bufferedReader = new BufferedReader(new InputStreamReader(in));
                        StringBuilder responseData = new StringBuilder();
                        String line;
                        while((line = bufferedReader.readLine()) != null){
                            responseData.append(line);
                        }
                        showResponse(responseData.toString());
                    }catch (Exception e){
                        e.printStackTrace();
                    }finally {
                        if (bufferedReader != null){
                            try{
                                bufferedReader.close();
                            }catch (IOException e){
                                e.printStackTrace();
                            }
                        }
                        if (connection != null){
                            connection.disconnect();
                        }
                    }
                }
            }).start();
        }
    
        private void showResponse(final String  responseData){
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    responseText.setText(responseData);
                }
            });
        }
    }
    

    当然,因为我们要访问网络资源,所以需要在AndroidManifest文件中声明一下网络权限。

    
    
        
           
          ...
        
    
    
  • 使用OkHttp
    OkHttp是Square公司开源的一个网络通信库,用来替代HttpURLConnection。项目地址为:https://github.com/square/okhttp。下面介绍一下它的基本使用:

    1. 在项目中添加OkHttp库的依赖:打开app/build.gradle文件,在dependencies闭包中添加
    implementation 'com.squareup.okhttp3:okhttp:3.11.0'
    

    这时系统会提示 Sync Now,点一下就行了。

    1. 创建OkHttpClient实例
    OkHttpClient client = new OkHttpClient();
    
    1. 创建Request对象。
      如果是GET请求:
    Request request = new Request.Bulider().url("https://www.baidu.com").build();
    

    如果是发起POST请求,我们需要先构造一个RequestBody对象来存放待提交的参数。

    RequestBody requestBody = new FormBody.Builder()
                          .add("username","admin").add("password","123456").build();
    Request request = new Request.Builder().url("http://www.baidu.com")
                          .post(requestBody).build();
    
    1. 接下来调用OkHttpClient的newCall()方法创建一个Call对象,并调用它的execute()方法来发送请求并获取服务器返回的数据。
    Response response = client.newCall(request).execute();
    String responseData = response.body().string();
    
    1. 最后要做的就是对返回的数据进行处理啦。比HttpURLConnection使用起来方便很多有没有!
      下面我们用OkHttp来实现一遍前面用HttpURLConnection实现的功能,其实就是修改一下sendRequestWithHttpURLConnection()方法,其余的部分不变,下面看代码:
    public void sendRequestWithHttpURLConnection(final String link){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder().url(link).build();
                    Response response = client.newCall(request).execute();
                    if (response.body() != null) {
                        String responseData = response.body().string();
                        showResponse(responseData);
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }).start();
    }
    
  • 网络编程实践
    前面学习了HttpURLConnection和OkHttp的用法,我们知道了如何发起HTTP请求,以及解析服务器返回的数据。但是在实际项目中,可能很多地方都会使用到网络请求,而发送HTTP请求的代码基本都是相同的。因此,通常情况下我们应该将这些通用的网络操作提取到一个公共的类里面,并提供一个静态方法,当要发起网络请求时,只需要调用一下这个方法即可。例如像下面这样,定义一个HttpUtil工具类,在里面定义一个静态public方法,用来实现网络请求:

    public class HttpUtil {
    
        public static String sendHttpRequest(String address){
            HttpURLConnection connection = null;
            try {
                URL url = new URL(address);
                connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("GET");
                connection.setConnectTimeout(8000);
                connection.setReadTimeout(8000);
                InputStream in = connection.getInputStream();
                BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                StringBuilder response = new StringBuilder();
                String line;
                while((line = reader.readLine()) != null){
                    response.append(line);
                }
                return response.toString();
            } catch (Exception e) {
                e.printStackTrace();
                return e.getMessage();
            }finally {
                if (connection != null){
                    connection.disconnect();
                }
            }
        }
    }
    

    当我们需要发起一个Http请求时,只需要两行代码:

    String address = "https://www.baidu.com";
    String response = HttpUtil.sendHttpRequest(address);
    

    很简单,不过这里有一个很严重的问题。我们知道发起网络请求是一个耗时操作,而耗时操作是不能在主线程中进行的,那如果我们在 sendHttpRequest() 方法中开启一个线程呢?这样网络请求确实是在子线程中进行的,但是主线程和子线程是异步执行的,也就是说,当主线程执行到 String response = HttpUtil.sendHttpRequest(address);这条语句时,子线程开启了,但是主线程并不会等子线程执行完,也就意味着 response 不会被赋值,我们无法获得返回的数据。(这里要好好想一想,理解清楚)
    在这种情况下,Java的回调机制就派上用场了。下面看如何使用回调机制来解决这个问题:

    1. 首先,我们要定义一个接口。
    public interface HttpCallbackListener {
        void onFinish(String response);
        void onFailure(Exception e);
    }
    

    我们在接口中定义了两个方法,onFinish()方法在请求成功时回调,参数为清楚返回的数据;onFailure()方法在请求失败时回调,参数为错误信息。

    1. 接下来,修改HttpUtil类中的 sendHttpRequest() 方法:
    public static void sendHttpRequest(final String address, final HttpCallbackListener listener){
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                try {
                    URL url = new URL(address);
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream in = connection.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                    StringBuilder response = new StringBuilder();
                    String line;
                    while((line = reader.readLine()) != null){
                        response.append(line);
                    }
                    if (listener != null){
                        listener.onFinish(response.toString());
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    if (listener != null){
                        listener.onFailure(e);
                    }
                }finally {
                    if (connection != null){
                        connection.disconnect();
                    }
                }
            }
        }).start();
    }
    

    乍一看,这不就是在sendHttpRequest() 方法里面创建了一个线程吗?不过仔细看看,是有些改动的。

    1. 首先,我们将方法改成了 void 类型,因为我们不需要通过这个方法返回数据了。
    2. 在参数中添加了一个 我们定义的 HttpCallbackListener listener 。
    3. 在return response 的地方,我们改成了 listener.onFinish(response.toString()); ,在请求成功时,我们通过调用接口的 onFinish() 方法,将返回数据传递出去。
    4. 在 return e.getMessage() 的地方,改成 listener.onFailure(e); ,在请求失败时,通过调用接口的 onFailure() 方法将错误信息传递出去。
      如此一来,我们就利用回调机制巧妙地将响应数据返回给了调用方。

    当我们需要发送Http请求的时候,只需要这样写:

        String address = "https://www.baidu.com";
        HttpUtil.sendHttpRequest(address, new HttpCallbackListener() {
            @Override
            public void onFinish(String response) {
                
            }
    
            @Override
            public void onFailure(Exception e) {
    
            }
        });
    

    这样是不是思路清晰很多了,在子线程中,当请求成功时,就会回调onFinish() 方法,并把相应数据传递出来,我门可以在 onFinish() 方法中地数据进行处理。当然,还需要注意一个问题,我们的 onFinish() 方法是在子线程中调用的,但是如果我们要像前面一样用一个TextView将数据显示出来,这就涉及到更新UI的操作了,我们需要回到主线程来进行UI更新的操作,我们可以借助 runOnUiThread() 方法来实现:

        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // to do something
            }
        });
    

    在这个 run() 方法里的代码是回到主线程执行的。

    那么如果要用 okhttp 来实现上面的简单封装,该如何操作呢?okhttp 库中是自带回调接口的,所以用起来更方便。

    public void sendOkHttpRequest(final String address, okhttp3.Callback callback){
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(address).build();
        client.newCall(request).enqueue(callback);
    }
    

    这里我们通过 enqueue() 方法传入一个 Callback 对象,在这个方法里面,Okhttp会开启子线程。
    调用的时候和HttpUrlConnection的实现方式一样:

        HttpUtil.sendOkHttpRequest(address, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                
            }
    
            @Override
            public void onResponse(Call call, Response response) throws IOException {
    
            }
        });
    

关于JSON数据解析

前面学会了如何发送请求Http请求,从服务器获取数据,但是我们总不能每次都用一个TextView把数据显示出来吧!当我们从一个接口请求数据时,返回的数据可能是多种格式的,现在主流的是 JSON 数据,当然也有一些XML格式的数据,不过很少见。下面我们直接通过一个Demo来学习如何解析Json数据吧。
这里安利一个很好的网站 玩Android ,是鸿洋大神维护的一个网站,里面收录了几个国内Android圈子里大神的公众号文章,而且提供了一系列免费接口 玩Android 开放API 供我们使用,可谓是 Android 学习者的福音啊。

下面这个是首页文章的接口:
Android基础回顾(八)| 使用HTTP协议访问网络_第1张图片

点进链接看一下JSON数据的结构:(我这里用的是 chrome 的 JSON-handle 插件)
Android基础回顾(八)| 使用HTTP协议访问网络_第2张图片
  • 使用 JSONObject 解析
    我们选择解析出author 、title 、tags下面的name、niceDate以及link这几个数据。下面来新建一个Model类,命名为Article,然后创建好显示解析出的数据对应的RecyclerView 的 item,命名为item_article(很简单,就不贴代码了,后面会附上代码链接),接下来在Activity的布局中添加一个RecyclerView。接下来就是最重要的两部分了,先为RecyclerView创建一个Adapter:
    public class ArticleAdapter extends RecyclerView.Adapter {
    
        private List
    articleList; private Context mContext; public ArticleAdapter(List
    articleList, Context mContext) { this.articleList = articleList; this.mContext = mContext; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) { View view = LayoutInflater.from(mContext).inflate(R.layout.item_article, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) { Article article = articleList.get(position); viewHolder.tvTitle.setText(article.getTitle()); viewHolder.tvAuthor.setText(article.getAuthor()); if (article.getTagName() != null){ viewHolder.tvTag.setText(article.getTagName()); viewHolder.tvTag.setVisibility(View.VISIBLE); }else { viewHolder.tvTag.setVisibility(View.GONE); } viewHolder.tvDate.setText(article.getDate()); viewHolder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(mContext, WvMainActivity.class); intent.putExtra("link", article.getLink()); mContext.startActivity(intent); } }); } @Override public int getItemCount() { return articleList.size(); } class ViewHolder extends RecyclerView.ViewHolder { @BindView(R.id.tv_title) TextView tvTitle; @BindView(R.id.tv_author) TextView tvAuthor; @BindView(R.id.tv_tag) TextView tvTag; @BindView(R.id.tv_date) TextView tvDate; View itemView; ViewHolder(@NonNull View itemView) { super(itemView); this.itemView = itemView; ButterKnife.bind(this, itemView); } } }
    然后在Activity中编写逻辑控制,首先初始化RecyclerView:
        LinearLayoutManager manager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(manager);
        mRecyclerView.addItemDecoration(new DividerItemDecoration(this ,DividerItemDecoration.VERTICAL));
        articleList = new ArrayList<>();
        adapter = new ArticleAdapter(articleList, this);
        mRecyclerView.setAdapter(adapter);
    
    然后使用我们前面写的 sendOkhttpRequest() 方法请求数据:
        String address = "http://www.wanandroid.com/article/list/0/json";
        HttpUtil.sendOkHttpRequest(address, this);
    
    这里为什么是 this,而不是 new CallBack() 呢?因为我让当前Activity实现了CallBack接口 implements Callback ,因此,可以对接口中的两个方法单独进行重写:
    @Override
    public void onFailure(@NonNull Call call, @NonNull IOException e) {
    
    }
    
    @Override
    public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
        articleList.clear();
        try {
            String jsonData = null;
            if (response.body() != null){
                jsonData = response.body().string();
            }
            JSONObject mJsonObject = new JSONObject(jsonData);
            JSONObject data = mJsonObject.getJSONObject("data");
            JSONArray mJsonArray = data.getJSONArray("datas");
            for (int i = 0; i < mJsonArray.length(); i++){
                JSONObject item = mJsonArray.getJSONObject(i);
                String title = item.getString("title");
                String author = item.getString("author");
                JSONArray tags = item.getJSONArray("tags");
                String tag = null;
                if (tags.length() > 0){
                    tag = tags.getJSONObject(0).getString("name");
                }
                String date = item.getString("niceDate");
                String link = item.getString("link");
                articleList.add(new Article(title, author, tag, date, link));
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }finally {
            initData();
        }
    }
    
    在 onResponse 方法中,我们首先从返回的 Response 对象中获取到返回的JSON数据 response.body().string(),然后就是解析的过程了,我们这里使用的是 JSONObject,还有一种方法是使用 Gson,后面再说。
    1. 我们再来具体分析一下这个接口返回的JSON数据的格式:
      Android基础回顾(八)| 使用HTTP协议访问网络_第3张图片

      最外层是一个 JSONObject(花括号“{”表示 JSONObject),然后它里面有一个名为 data 的JSONObject,然后里面的 curPage 是一个 String 对象,还有一个 datas 是JSONArray(方括号“[”表示JSONArray),然后再里层就是多个JSONObject,这些JSONObject就是我们需要获取的每一篇文章的数据。

    2. 既然知道JSON数据的结构,解析起来就容易了,首先创建一个JSONObject对象,将 jsonData 作为参数传入,那么这个JSONObject对象就是最外层的JSONObject了;然后调用它的 getJSONObject() 方法,这个方法接收一个String参数,表示 JSONObject 的 name,我们传入 “data”;再下一层是一个JSONArray,我们调用 上一层JSONObject 的getJsonArray() 方法,同样传入这个JSONArray的 name,即 “datas”;既然是一个JSONArray,我们就要来遍历获取里面的每一条 Article 数据了。调用 JSONArray 的 getJSONObject() 方法,这个方法接收一个int参数,也就是JSON数组中JSON对象的 索引,从0开始。然后JSONObject提供了一系列get方法的重载,我们要获取的数据都是 String 类型,所以我们调用它的 getString() 方法,传入对应数据的 name,就能获取到该数据的value了。 这样我们就解析完成了。
    3. 最后数组中的数据更新了,我们调用 ArticleAdapter对象的 notifyDataSetChanged() 方法,就可以更新列表数据了。
    private void initData() {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                adapter.notifyDataSetChanged();
                loadingDialog.dismiss();
            }
        });
    }
    

    接下来看看运行效果:

示例代码地址:samples

  • 使用Gson解析
    GSON 是google 提供的JSON数据解析开源库,使用GSON解析JSON数据更加便捷、更加简单,下面来看看GSON的基本使用。
    首先需要添加依赖:

    implementation 'com.google.code.gson:gson:2.8.5'
    

    GSON库之所以能让我们解析JSON数据更加便捷,在于它可以将一段JSON数据自动映射成一个对象,而不需要我们逐项进行解析。因此,我们需要定义一个能够和JSON数据的结构对应的实体类。而前面定义的实体类明显不满足这个要求,我们需要来进行修改,先来看看JSON数据的结构:
    Android基础回顾(八)| 使用HTTP协议访问网络_第4张图片

    我们需要解析出author、link、niceDate、tags里面的name以及title这五个字段,其中niceDate以及tags里面的name这两个字段和我们的model里面命名不同,并且这里的tags是一个JSONArray,知道了这两点不同,我们再来修改:

    public class Article {
    
        private String author;
        private String link;
        @SerializedName("niceDate")
        private String date;
        @SerializedName("tags")
        private List tags;
        private String title;
    
        public class Tag {
            String name;
        }
    }
    

    对于和JSON数据里面命名相同的字段,我们可以不用修改,而命名不同的,我们可以用GSON提供的 @SerializedName 注解的方式将JSON字段和Java实体类字段之间建立映射关系,这样GSON解析的时候就能自动对应了;然后就是这个tags字段里面的name字段了,这里比较特殊,tags是一个JSONArray,所以我们需要用一个List来与之对应,并且,我们还需要新建一个model来与这个JSONArray中的每一个子项对应,因此,我们新建一个内部类 Tag,由于我们只需要解析 name 这个字段,因此,我们给它定义一个name属性(有必要的话,这里也可以使用@SerializedName 注解),这样,我们的实体类定义就完成了。下面来看如何解析:

    Gson gson = new Gson();
    articleList = gson.fromJson(jsonData, new TypeToken>(){}.getType());
    

    这里因为我们需要解析的是一个JSONArray(多条文章数据),因此需要借助 TypeToken 来获取我们 实例化 List 的数据类型,并将这个类型传入到 fromJson方法中。如果我们解析的是一个JSONObject,直接传入实体类的 class 对象即可:

    Article article = gson.fromJson(jsonData, Article.class);
    

    当然,GSON的使用十分灵活,这里只给出了最基本的解析方法,我们还需要更深入的了解其相关内容,才能应对各种场景。


上一篇:Android基础回顾(七)| 使用手机多媒体
下一篇:Android基础回顾(九)| Android多线程编程


你可能感兴趣的:(Android基础回顾(八)| 使用HTTP协议访问网络)