【筆記】Android 網路

weixin_34320159發表於2018-06-22

《第一行程式碼》個人筆記系列

3787831-e6b3a199da315b7a.png
目錄

1 WebView簡單用法

PS:這裡書中只介紹WebView基本用法,過於簡單,並不全面,過一遍例子即可,今後抽空會詳解WebView

  • 需求:比如說需要在程式裡展示一些網頁,但是又明確指明不允許開啟系統瀏覽器,這裡我們就可以用WebView來實現內嵌一個網頁
    比如:我們簡單開啟一個百度的網頁
  • 首先,佈局檔案簡單放入一個webview
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <WebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </WebView>
</LinearLayout>
  • 再看MainActivity
package com.example.kt.webviewdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        WebView webView = findViewById(R.id.web_view);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("http://www.baidu.com");
    }
}

  • 可以看到,先是獲取WebView 的例項
  • 然後呼叫getSettings()方法進行了一些設定,比如setJavaScriptEnabled(true)讓它支援JavaScript指令碼 (響應一些網頁點選、下載事件)
  • 再呼叫setWebViewClient()方法並傳入一個WebViewClient例項 (這段程式碼的作用是:當需要從一個網頁跳轉到另一個網頁的時候,我們希望目標網頁仍然在當前的WebView中顯示,而不是開啟系統瀏覽器)
  • 最後再通過loadUrl()傳入需要載入的網址
  • 還有由於請求了網路,別忘記加許可權
<uses-permission android:name="android.permission.INTERNET"/>

執行效果:


3787831-ebe4144be521c980.png

2 使用HTTP協議訪問網路

2-1 使用HttpURLConnection (基本用法)

Android 傳送 HTTP 請求一般有兩種方式:
HttpURLConnection 和 HttpClient

不過 HttpClient 存在 API 數量過多、擴充套件困難等缺點
且在 Android 6.0系統被完全移除,因此官方更加推薦我們使用 HttpURLConnection

  • 直接上例子吧
  • 新建一個專案NetWorkDemo
  • 先來看佈局檔案,簡單放入一個按鈕用於發起網路請求,一個TextView用於顯示請求得到的資料,一個ScollView用於支援上下滑動,因為得到的資料可能不止一頁
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/send_request"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="發出請求" />
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/response_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="" />
    </ScrollView>
</LinearLayout>
  • 再來看MainActivity程式碼
package com.example.kt.networkdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    TextView responseText;
    Button sendRequest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        responseText = findViewById(R.id.response_text);
        sendRequest = findViewById(R.id.send_request);
        sendRequest.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.send_request:
                sendRequestWithHttpURLConnection();
                break;
        }
    }

    private void sendRequestWithHttpURLConnection(){
        //開啟執行緒發起網路請求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("http://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream in = connection.getInputStream();
                    //下面對獲取到輸入流進行讀取
                    reader = new BufferedReader(new InputStreamReader(in));
                    StringBuilder response = new StringBuilder();
                    String line = "";
                    while((line = reader.readLine()) != null){
                        response.append(line);
                    }
                    showResponse(response.toString());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if(reader != null){
                        try {
                            reader.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if(connection != null){
                        connection.disconnect();
                    }
                }
            }
        }).start();
    }

    private void showResponse(final String response){
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // 在這裡進行UI操作,將結果顯示到介面上
                responseText.setText(response);
            }
        });
    }
}
  • 點選按鈕後發起網路請求,需要開個子執行緒來處理網路請求的耗時任務
  • 首先新建URL物件,並傳入網址
    通過openConnection()並強轉拿到 HttpURLConnection 例項
  • 然後設定一些方法
    setRequestMethod()設定請求方式是GET還是POST
    setConnectTimeout()設定連線超時時間
    setReadTimeout()設定讀取超時時間
  • 再通過getInputStream()拿到返回的輸入流
    接著對獲取到的輸入流進行讀取
    InputStream為引數拿到InputStreamReader (輸入流讀取器)
    InputStreamReader為引數拿到BufferedReader(緩衝讀取器)
    最後呼叫readLine()一行行去讀取返回的資料,並用 StringBuilder 儲存起來
  • 在 finally 裡面我們記得進行收尾工作,BufferedReader 用完後需要及時 關閉,HttpURLConnection 也需要在最後斷開連線
  • 最後呼叫 showResponse方法來展示 獲取到的返回資料,由於只有主執行緒才能更新UI 因此呼叫 runOnUiThread()來處理
  • 當然,由於用到了網路請求,別忘記在配置檔案中加許可權
<uses-permission android:name="android.permission.INTERNET"/>
  • 執行效果:


    3787831-48f838b01614e10c.png

我們剛剛演示的是GET請求方式,如果需要使用POST該怎麼辦呢?
(書中POST部分沒有詳細程式碼,需要大家自行網頁搜尋完成)

3787831-6488fbdc36fda5fe.png
2-2 使用OkHttp(基本用法)

OkHttp是 Square 公司貢獻的一個網路開源庫
簡單易用、是目前主流的網路通訊庫之一
該公司還開發了 Picasso、Retrofit等著名的開源專案

  • 首先在我們的app/build.gralde里加入對OkHttp的依賴,暫時用3.4.1的版本
    (這裡僅出於演示,截止2018年6月,目前最新的版本好像是3.10.0)
compile 'com.squareup.okhttp3:okhttp:3.4.1'
  • 直接上程式碼,接上一節的例子
    這次只是把請求方式由 HttpURLConnection 改為 OkHttp
package com.example.kt.networkdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    TextView responseText;
    Button sendRequest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        responseText = findViewById(R.id.response_text);
        sendRequest = findViewById(R.id.send_request);
        sendRequest.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.send_request:
                sendRequestWithOkHttp();
                break;
        }
    }

    //改為OkHttp方式請求網路
    private void sendRequestWithOkHttp(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder()
                            .url("https://www.baidu.com")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();
                    showResponse(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private void showResponse(final String response){
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // 在這裡進行UI操作,將結果顯示到介面上
                responseText.setText(response);
            }
        });
    }
}
  • 主要來看sendRequestWithOkHttp()方法
  • 同樣是開個子執行緒來處理網路請求這樣的耗時任務
  • 建立一個 OkHttpClient 例項
  • 如果想傳送一條HTTP請求,就需要建立一個 Request 物件
    可以在最終build()之前連綴很多方法來豐富這個 Request 物件
    這裡用 url() 並傳入 目標網址
  • 然後呼叫 OkHttpClient 的 newCall()方法建立一個Call物件,並呼叫execute()來傳送請求並獲取到伺服器返回的資料
    即 Response 物件
  • 最後呼叫 body().string()把結果轉換成字串並在介面顯示出來
  • 執行效果和 2-1 例子相同

3 搭建簡單的Web伺服器

每個需要訪問網路的應用程式都需要有自己的伺服器
我們可以提交資料到伺服器(POST),又或者從伺服器獲取資料(GET)
隨便傳一段文字肯定是不行的,最好能從中知道資料的用途
因此,一般我們會在網路上傳輸一些格式化後的資料,這種會有一定的結構規格和語義,當另一方收到資料訊息後就可以按相同的結構規格和語義來解析資料,從而取出他想要得到的部分

目前網路傳輸資料主流的有兩種: XMLJSON

  • 下面我們就先來搭建一個簡單的伺服器
    當然伺服器型別很多可以選擇,我們這裡選擇 Apache
  • 下載和安裝步驟就暫時省略了,自行百度 "Apache伺服器下載",根據系統隨便下載一個安裝包並一路Next即可

我這裡用的是Windows版 ,並把安裝目錄指定到了E盤

  • 安裝成功後,在瀏覽器輸入 127.0.0.1
    如果出現下圖就表示伺服器已經成功啟動了
    3787831-c4cf0396f7a1e0e1.png

接下來我們要在伺服器內放入XML資料
等下發請求呼叫就會返回資料給app客戶端

  • 找到伺服器目錄E:\apache\htdocs,並新建一個名為 get_data.xml 檔案
    並加入以下XML內容
<apps>
     <app>
         <id>1</id>
         <name>Google Maps</name>
         <version>1</version>
     </app>
     <app>
         <id>2</id>
         <name>Chrome</name>
         <version>2.1</version>
     </app>
     <app>
         <id>3</id>
         <name>Google Play</name>
         <version>2.3</version>
     </app>
</apps>
  • 此時再次開啟瀏覽器輸入網址:http://127.0.0.1/get_data.xml
    可以看到以下結構

    3787831-10ac0873fe377457.png

  • 之前的網址"127.0.0.1"只是PC端的本地伺服器地址
    我們實體手機一般都是連的公司WIFI,因此當然無法訪問自己PC的伺服器
    後面的資料解析,我們需要改為模擬器訪問
    因此網址變為了"http://10.0.2.2/get_data.xml"
    因為10.0.2.2對於模擬器來說就是電腦本機的IP地址

注意:
經過測試,有以下情況
【1】用自帶手機無法連線到電腦本地伺服器
(除非PC自帶WIFI,而手機連的是PC發出的無線WIFI,需要你自己的電腦擁有無線網路卡,感興趣的還請自行搜尋360免費wifi或者其他相關攻略)
【2】Genymotion或夜神模擬器也無法通過 10.0.2.2的網址來訪問本地伺服器

  • 因此選用第三種,也是最快最簡單的一種
    就是使用AndroidStudio原生的模擬器來訪問
  • 測了一下效果,可以訪問,說明沒問題,這樣準備工作就算真正完成了
    接下來章節我們會講解如何獲取並解析這些資料


    3787831-816b6c833c1d80b6.png
    AS自帶模擬器可以訪問PC端本地伺服器地址

4 解析XML格式資料

4.1 Pull解析方式

同樣接第二節的例子,由於已經完成了請求網路的部分
我們可以把精力放在解析資料上
我勒個去,終於可以解析了

  • 直接上程式碼
package com.example.kt.networkdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;

import java.io.StringReader;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    TextView responseText;
    Button sendRequest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        responseText = findViewById(R.id.response_text);
        sendRequest = findViewById(R.id.send_request);
        sendRequest.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.send_request:
                sendRequestWithOkHttp();
                break;
        }
    }

    //改為OkHttp方式請求網路
    private void sendRequestWithOkHttp(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder()
                            .url("http://10.0.2.2/get_data.xml")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();
                    parseXMLWithPull(responseData);

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //Pull解析XML資料
    private void parseXMLWithPull(String xmlData){
        try {
            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
            XmlPullParser xmlPullParser = factory.newPullParser();
            xmlPullParser.setInput(new StringReader(xmlData));
            int eventType = xmlPullParser.getEventType();
            String id = "";
            String name = "";
            String version = "";
            while (eventType != XmlPullParser.END_DOCUMENT) {
                String nodeName = xmlPullParser.getName();
                switch (eventType){
                    //開始解析某個節點
                    case XmlPullParser.START_TAG:{
                        if("id".equals(nodeName)){
                            id = xmlPullParser.nextText();
                        } else if("name".equals(nodeName)){
                            name = xmlPullParser.nextText();
                        } else if("version".equals(nodeName)){
                            version = xmlPullParser.nextText();
                        }
                        break;
                    }
                    //完成解析某個節點
                    case XmlPullParser.END_TAG:{
                        if("app".equals(nodeName)){
                            Log.i("TAG","id is "+id + " name is "+name +" version is "+version );
                        }
                        break;
                    }
                    default:
                        break;
                }
                eventType = xmlPullParser.next();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 首先來看okhttp請求,地址已經換成了 .url("http://10.0.2.2/get_data.xml")
  • 然後在拿到返回的資料 responseData 後就開始呼叫parseXMLWithPull()方法對XML進行解析
  • 我們具體來看Pull解析部分:
    先要得到一個 XmlPullParserFactory 的例項
    然後藉助這個例項拿到 XmlPullParser 物件
    再呼叫 XmlPullParser 的 setInput()方法將 伺服器返回的XML資料設定進去進行解析
  • 再來看解析過程:
    先是通過getEventType()拿到當前解析事件
    然後進入到一個while迴圈不斷地進行解析
    如果當前事件不等於XmlPullParser.END_DOCUMENT,則解析工作還沒有完成,呼叫xmlPullParser.next()繼續獲取下一個解析事件
    通過xmlPullParser.getName()獲取當前節點的名字,如果節點名字為idnameversion,則通過nextText()拿到值
    而每當解析完一個 app 節點後,就把它們列印出來
  • 執行效果:


    3787831-554c3aede5fda923.png
4.2 SAX解析方式

SAX 用法比 Pull 複雜一些,但是語義方面會更加清楚
我們使用需要SAX解析XML,主要藉助 DefaultHandler 類

  • 直接來看程式碼吧,
    新建一個類ContentHandler繼承自DefaultHandler並重寫其5個方法
package com.example.kt.networkdemo;


import android.util.Log;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class ContentHandler extends DefaultHandler{

    private String nodeName;
    private StringBuilder id;
    private StringBuilder name;
    private StringBuilder version;

    @Override
    public void startDocument() throws SAXException {
        id = new StringBuilder();
        name = new StringBuilder();
        version = new StringBuilder();
    }

    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        //記錄當前節點名
        nodeName = localName;
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if("app".equals(localName)){
            Log.i("ContentHandler", "id is " + id.toString().trim());
            Log.i("ContentHandler", "name is " + name.toString().trim());
            Log.i("ContentHandler", "version is " + version.toString().trim());
            //最後要將 StringBuidler清空掉
            id.setLength(0);
            name.setLength(0);
            version.setLength(0);
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        //根據當前的節點名判斷將內容新增到哪個StringBuilder中
        if("id".equals(nodeName)){
            id.append(ch,start,length);
        }else if("name".equals(nodeName)){
            name.append(ch,start,length);
        }else if("version".equals(nodeName)){
            version.append(ch,start,length);
        }
    }
}
  • startDocument方法:在開始整個XML解析的時候呼叫
    startElement方法:在開始解析某個節點的時候呼叫
    characters方法:在獲取節點中內容的時候呼叫
    endElement方法:在完成解析某個節點的時候呼叫
    endDocument方法:在完成整個XML解析的時候呼叫
  • 從XML中解析的資料會以引數的形式傳到
    startElementcharactersendElement 方法中
  • 而在獲取節點的時候, characters方法也可能會被呼叫多次
  • 首先給id、name、version 分別定義了一個 StringBuilder 物件
    並且在 startDocument方法裡進行了初始化
  • 然後會開始解析某個節點,此時startElement方法會被呼叫,其中localName記錄著當前節點的名字,我們需要把它記錄下來
  • 接著在解析節點的具體內容的時候會呼叫 characters方法,我們會根據當前節點名進行判斷,將解析出的內容新增到哪個StringBuilder物件中
  • 最後會執行 endElement方法,我們會判斷如果app節點已經解析完成,就列印出 id 、 name、version資料
    由於它們可能包括回車和換行,因此還需要呼叫一下 trim()方法
    並且在列印完成後還要將 StringBuidler的內容清空,否則會影響下一次內容的讀取
  • 當全部節點解析完成後,最終呼叫endDocument收尾,很顯然我們在這裡什麼都沒有做

OK,接下里的程式碼就比較簡單,接之前的案例,只需要把解析形式從Pull替換成SAX即可

package com.example.kt.networkdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;

import java.io.StringReader;

import javax.xml.parsers.SAXParserFactory;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    TextView responseText;
    Button sendRequest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        responseText = findViewById(R.id.response_text);
        sendRequest = findViewById(R.id.send_request);
        sendRequest.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.send_request:
                sendRequestWithOkHttp();
                break;
        }
    }

    //改為OkHttp方式請求網路
    private void sendRequestWithOkHttp(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder()
                            .url("http://10.0.2.2/get_data.xml")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();
                    parseXMLWithSAX(responseData);

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //SAX解析XML資料
    private void parseXMLWithSAX(String xmlData){
        try {
            SAXParserFactory factory = SAXParserFactory.newInstance();
            XMLReader xmlReader = factory.newSAXParser().getXMLReader();
            ContentHandler handler = new ContentHandler();
            //將 ContentHandler的例項設定到XMLReader中
            xmlReader.setContentHandler(handler);
            //開始執行解析
            xmlReader.parse(new InputSource(new StringReader(xmlData)));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 在得到伺服器返回的資料後,我們呼叫parseXMLWithSAX方法來解析XML資料
  • 先是建立了一個 SAXParserFactory物件
    然後藉助它拿到 XMLReader物件
    新建一個ContentHandler例項並設定到 xmlReader 中
    最後呼叫parse()解析就可以了
  • 執行效果同 4.1 一樣,就不上圖了

此外還有一種 DOM 解析方式,感興趣的可以自行查詢,這裡不贅述了

5 解析JSON格式資料

比起XML
JSON的優勢在於:體積更小 、在網路傳輸時更省流量
缺點在於語義性較差,不如XML直觀
(但是我們一般可以利用工具進行JSON格式化,因此不成問題,且JSON在開發過程中更為常見)

  • 同樣先是在E:\apache\htdocs裡建立一個get_data.json的檔案
    加入json程式碼
[{"id":"5","version":"5.5","name":"Clash of Clans"},{"id":"6","version":"7.0","name":"Boom Beach"},{"id":"7","version":"3.5","name":"Clash Royale"}]
  • 如果覺得語義性差,可以通過線上JSON格式化工具來觀察,其實挺直觀的
    如: https://www.json.cn/
    3787831-1298392c0460ad99.png
  • 方括號代表一個List,花括號代表一個bean物件,而每個bean有三個屬性,如果對JSON不熟悉,還請自行搜尋相關資料
5-1 使用JSONObject解析

解析JSON的方法也有很多,可以使用官方的 JSONObject ,也可以使用 Google的 Gson

還是接第二節的例子
只不過把資料從 XML 換成了JSON,解析方式也改為 JSONObject

  • 直接上程式碼
package com.example.kt.networkdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    TextView responseText;
    Button sendRequest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        responseText = findViewById(R.id.response_text);
        sendRequest = findViewById(R.id.send_request);
        sendRequest.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.send_request:
                sendRequestWithOkHttp();
                break;
        }
    }

    //改為OkHttp方式請求網路
    private void sendRequestWithOkHttp(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder()
                            .url("http://10.0.2.2/get_data.json")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();
                    parseXMLWithJSONObject(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //JSONObject解析JSON資料
    private void parseXMLWithJSONObject(String jsonData){
        try {
            JSONArray jsonArray = new JSONArray(jsonData);
            for(int i=0 ; i<jsonArray.length(); i++){
                JSONObject jsonObject = jsonArray.getJSONObject(i);
                String id = jsonObject.getString("id");
                String name = jsonObject.getString("name");
                String version = jsonObject.getString("version");
                Log.i("MainActivity","id is "+ id);
                Log.i("MainActivity","name is "+ name);
                Log.i("MainActivity","version is "+ version);
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
}
  • 首先把 url地址換成了"http://10.0.2.2/get_data.json"
  • 然後在得到伺服器返回的資料後呼叫parseXMLWithJSONObject()來解析
  • 由於我們的JSON資料在伺服器最外層定義的是一個陣列,因此傳到一個JSONArray物件中,然後迴圈遍歷這個JSONArray ,每個元素都是一個
    JSONObject物件,而每個物件又包含三個值:id 、name 、version
  • 通過getString()取出資料,拿到以後我們就列印出來即可
    可見JSON資料解析確實非常簡單
  • 執行效果:
    3787831-9e8c31312e3cb4eb.png
5-2 使用GSON解析

GSON 比 JSONObject 更為簡單
不過 GSON 並沒有新增到官方的API中,如果想要使用的話,需要加入依賴

  • app/build.gradle中加入依賴
compile 'com.google.code.gson:gson:2.7'
  • GSON 的特點在於可以直接轉換成物件
    我們可以提前定義好實體類


    3787831-a5defa974a50c760.png
  • 直接上程式碼
  • 首先新增一個App實體類,並寫好 get 和 set 方法
package com.example.kt.networkdemo;

public class App {
    private String id;
    private String name;
    private String version;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }
}
  • 再來看MainActivity
package com.example.kt.networkdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import java.util.List;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    TextView responseText;
    Button sendRequest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        responseText = findViewById(R.id.response_text);
        sendRequest = findViewById(R.id.send_request);
        sendRequest.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.send_request:
                sendRequestWithOkHttp();
                break;
        }
    }

    //改為OkHttp方式請求網路
    private void sendRequestWithOkHttp(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder()
                            .url("http://10.0.2.2/get_data.json")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();
                    parseXMLWithGSON(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //GSON解析JSON資料
    private void parseXMLWithGSON(String jsonData){
        Gson gson = new Gson();
        List<App> appList = gson.fromJson(jsonData,new TypeToken<List<App>>(){}.getType());
        for(App app : appList){
            Log.i("MainActivity","id is "+ app.getId());
            Log.i("MainActivity","name is "+ app.getName());
            Log.i("MainActivity","version is "+ app.getVersion());
        }
    }
}
  • 拿到伺服器返回的資料後解析改為了parseXMLWithGSON()
  • 注意看我們主要藉助了TypeToken,把需要轉換的資料型別傳了進去
    很容易理解,正如之前所說我們的JSON資料就像一個List<App>
    即一個陣列,裡面含有三個bean類
    這個bean類剛好又是我們自定義的App類
    而每個App類都擁有 id 、name、version屬性
  • 執行效果和 5-1 一樣,這裡不贅述

6 網路實踐:優化封裝一個簡單的網路框架

我們前幾節已經掌握了 HttpURLConnection 和 OkHttp 的用法
知道了發起 HTTP請求 以及 解析伺服器返回的資料
但是一個應用程式可能很多地方都用到了網路功能,而傳送HTTP請求的程式碼幾乎都是相同的,如果我們每一次請求都去編寫一遍相同的程式碼
顯然是非常差勁的做法

第一步:整合成工具類

  • 我們通常應該將這些網路操作提取到一個公共的類裡面
    並提供一個靜態方法,當想要發起網路請求的時候,只需要簡單呼叫即可
package com.example.kt.networkdemo;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

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);
            connection.setDoInput(true);
            connection.setDoOutput(true);
            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();
            }
        }
    }
}
  • 比如:
String address = "http://www.baidu.com";
String response = HttpUtil.sendHttpRequest(address);

第二步:子執行緒 + 介面回撥

注意:
網路請求都是耗時操作,而sendHttpRequest()並沒有開啟執行緒
這樣很容易導致主執行緒被阻塞
——————————————————————————
那給它開個子執行緒不就行了嗎?
並不能完全解決問題!如果我們僅僅只是在sendHttpRequest()內開個子執行緒來處理耗時邏輯,伺服器響應的資料是無法立刻返回的,而sendHttpRequest()會繼續往下執行程式碼,該方法會在伺服器還沒來得及響應的時候就執行結束了,當然無法拿到返回響應的資料。

  • 因此,我們還需加上 Java回撥!
  • 來看程式碼,首先定義一個介面 HttpCallbackListener
    onFinish 攜帶伺服器響應成功返回的資料,以便我們使用
    onError 攜帶異常資訊,以便我們列印
public interface HttpCallbackListener {

    void onFinish(String response);

    void onError(Exception e);

}
  • 修改 HttpUtil
  • 先給sendHttpRequest()新增了一個HttpCallbackListener介面物件作為引數
  • 然後開啟了一個子執行緒來處理網路請求耗時操作
  • 此外,需要注意的是,子執行緒內是無法通過 return 語句來返回資料的,所以我們改為回撥介面來回傳資料
  • 伺服器響應成功的時候:
    把返回的資料傳入介面HttpCallbackListeneronFinish()
    出現異常失敗的時候:
    把異常資訊傳入介面HttpCallbackListeneronError()
package com.example.kt.networkdemo;


import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class HttpUtil {

    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);
                    connection.setDoInput(true);
                    connection.setDoOutput(true);
                    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.onError(e);
                    }
                } finally {
                    if (connection != null) {
                        connection.disconnect();
                    }
                }
            }
        }).start();
    }
}
  • 然後在需要用到網路請求的時候,需要額外再傳入第二個引數HttpCallbackListener

注意:
回撥介面都是在子執行緒中執行的,因此不可以執行UI操作
如果需要更新UI,則建議使用 runOnUiThread 或 handler處理

        // 根據返回結果執行具體邏輯
        HttpUtil.sendHttpRequest("http://www.baidu.com", new HttpCallbackListener() {
            @Override
            public void onFinish(String response) {

            }

            @Override
            public void onError(Exception e) {

            }
        });

(可選項)第三步:OkHttp 替代 HttpURLConnection會更簡潔

  • 修改 HttpUtil
package com.example.kt.networkdemo;


import okhttp3.OkHttpClient;
import okhttp3.Request;

public class HttpUtil {

    public static void sendOkHttpRequest(String address , okhttp3.Callback callback) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(address)
                .build();
        client.newCall(request).enqueue(callback);
    }
}
  • 可以看到請求方式改為了 OkHttp
    然後新加入一個引數也是 OkHttp自帶的回撥介面 okhttp3.Callback
    最後在傳送請求的時候,client.newCall(request)沒有像之前一樣呼叫execute(),而是使用了enqueue()並傳入回撥介面
  • 事實上這裡 OkHttp在enqueue()的內部已經幫我們開好了子執行緒,並執行HTTP請求,並將返回結果回撥到okhttp3.Callback當中
  • 在需要用到網路請求的時候,額外傳入介面作為引數
        HttpUtil.sendOkHttpRequest("http://www.baidu.com", new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String responseData = response.body().string();
            }
        });
    }

再囉嗦一句:
不管是 OkHttp 還是 HttpURLConnection
回撥介面都是在子執行緒中執行的,因此不可以執行UI操作
如果需要更新UI,則建議使用 runOnUiThread 或 handler處理


參考:《第一行程式碼》

相關文章