如何使用OkHttp(kotlin)

如何使用OkHttp(kotlin)

情境

OkHttp 是 Square 出產的一個 Open source project,是一個很有效率的Http連線的第三方,一些有名的連線套件都是基於這個第三方加入更多的功能,例如Volley和Retrofit。

完整程式碼

你可以到 GitHub 上面觀看或者下載完整的程式碼

程式碼說明

它在使用上也非常的簡單,首先 build.gradle 上直接加以下程式碼。

implementation 'com.squareup.okhttp:okhttp:2.5.0'

記得網路權限也要開一下。

<uses-permission android:name="android.permission.INTERNET" />

建立一個XML。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.givemepass.okhttpdemo.MainActivity">
    <Button
        android:id="@+id/send_request"
        android:text="send request"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <ScrollView
        android:layout_below="@id/send_request"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!" />
    </ScrollView>
</RelativeLayout>

存在一個 Button,按下去處理 Request,當回來的 Response 就顯示在 TextView 上面。


接著透過程式碼去送 Request 給 Google Server,看看 Response 回來什麼東西。

val client = OkHttpClient()
val request = Request.Builder()  
        .url("http://www.google.com.tw")  
        .build()  
client.newCall(request).enqueue(object : Callback {  
    override fun onFailure(request: Request, e: IOException) {  
        runOnUiThread { text.text = e.message }  
  }  
  
    @Throws(IOException::class)  
    override fun onResponse(response: Response) {  
        val resStr = response.body().string()  
        runOnUiThread { text.text = resStr }  
  }  
})

首先建立一個OkHttpClient的物件,接著在透過Request類別去Builder出一個Request,網址我們就設定為 Google 首頁,再透過OkHttpClient 的 newCall 接收這個 Request,最後,利用 execute 方法去執行這個 Http Request,然後把回來的字串用TextView呈現出來。
這時候你就會發現出現這樣的 Exception。

Caused by: android.os.NetworkOnMainThreadException

怎麼一回事?
原來透過 execute 這個方法必須寫在 Worker Thread,所以我們就加上了 Thread 來執行我們的這個片段。

val service = Executors.newSingleThreadExecutor()
sendBtn.setOnClickListener{
 service.submit {  
  val builder = HttpUrl.parse("http://www.google.com").build()  
  try {  
   val response = client.newCall(request).execute()  
   val resStr = response.body().string()  
   runOnUiThread { text.text = resStr }  
  } catch (e: IOException) {  
   e.printStackTrace()  
  }  
 }
}

由於字串從 Response 回來還是處於 Worker Thread,從 Response 內取出 body 的字串,就是我們要的內容,因此可以透過 runOnUiThread 的方法更新 TextView 的內容。


根據以下程式碼的寫法,就可以加入參數到 Request 內。

val builder = HttpUrl.parse("https://www.google.com.tw/search?").newBuilder()  
builder.addQueryParameter("q", "givemepass")  
builder.addQueryParameter("oq", "givemepass")  
  
val request = Request.Builder()  
        .url(builder.toString())  
        .build()

就會看到 givemepass 的相關搜尋了。


你也可以加相關 auth 在 header 內。

val request = Request.Builder()
    .header("Authorization", "your token")
    .url(urlStr)
    .build()

透過下面的寫法。

val res = client.newCall(request).execute()

回來的是一個 Response,那就會出現一個疑問了,我可以改用 Callback 的方式嗎?當然可以。

client.newCall(request).enqueue(object : Callback {  
 override fun onFailure(request: Request, e: IOException) {  
  e.printStackTrace()
 }  

 @Throws(IOException::class)  
 override fun onResponse(response: Response) {  
  if (!response.isSuccessful()) {
   throw new IOException("Unexpected code " + response);
  } else {
   // do something wih the result
  }
 }  
})

透過 enqueue 可以傳入一個 Callback,透過 Callback 回來的方法,可以處理成功或失敗。

val request = Request.Builder()  
 .url("http://www.google.com.tw")  
 .build()  
client.newCall(request).enqueue(object : Callback {  
 override fun onFailure(request: Request, e: IOException) {  
  text.text = e.message 
 }  

 @Throws(IOException::class)  
 override fun onResponse(response: Response) {  
  val resStr = response.body().string()  
  text.text = resStr
 }  
})

居然閃退了,怎麼一回事?
看一下 log。

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

原來使用 enqueue 方法實作 callback,OkHttp 會幫你產生一個Worker Thread,因此回來的的 response 也是在 Worker Thread,如果你要更新 TextView 的畫面必須調整回 Main Thread。

client.newCall(request).enqueue(object : Callback {  
 override fun onFailure(request: Request, e: IOException) {  
  runOnUiThread { text.text = e.message }  
 }  

 @Throws(IOException::class)  
 override fun onResponse(response: Response) {  
  val resStr = response.body().string()  
  runOnUiThread { text.text = resStr }  
 }  
})

這樣一來就可以看到回來的畫面了。


這時候你就會想到, 網路最常使用的格式 JSON 可不可以處理?
當然可以。
透過以下網址取 JSON,
https://raw.githubusercontent.com/givemepassxd999/okhttp_demo/master/app/src/main/res/sample.json
回來會看到以下的資訊。

[ { "Name" : "Alfreds Futterkiste", "City" : "Berlin", "Country" : "Germany" }, { "Name" : "Berglunds snabbköp", "City" : "Luleå", "Country" : "Sweden" }, { "Name" : "Centro comercial Moctezuma", "City" : "México D.F.", "Country" : "Mexico" }, { "Name" : "Ernst Handel", "City" : "Graz", "Country" : "Austria" }, { "Name" : "FISSA Fabrica Inter. Salchichas S.A.", "City" : "Madrid", "Country" : "Spain" }, { "Name" : "Galería del gastrónomo", "City" : "Barcelona", "Country" : "Spain" }, { "Name" : "Island Trading", "City" : "Cowes", "Country" : "UK" }, { "Name" : "Königlich Essen", "City" : "Brandenburg", "Country" : "Germany" }, { "Name" : "Laughing Bacchus Wine Cellars", "City" : "Vancouver", "Country" : "Canada" }, { "Name" : "Magazzini Alimentari Riuniti", "City" : "Bergamo", "Country" : "Italy" }, { "Name" : "North/South", "City" : "London", "Country" : "UK" }, { "Name" : "Paris spécialités", "City" : "Paris", "Country" : "France" }, { "Name" : "Rattlesnake Canyon Grocery", "City" : "Albuquerque", "Country" : "USA" }, { "Name" : "Simons bistro", "City" : "København", "Country" : "Denmark" }, { "Name" : "The Big Cheese", "City" : "Portland", "Country" : "USA" }, { "Name" : "Vaffeljernet", "City" : "Århus", "Country" : "Denmark" }, { "Name" : "Wolski Zajazd", "City" : "Warszawa", "Country" : "Poland" } ]

好亂喔!不過沒關係,只要透過JSON Parser這個網站,就可以看到很整齊的 Json 格式。

[
 {
  "Name":"Alfreds Futterkiste",
  "City":"Berlin",
  "Country":"Germany"
 },
 {
  "Name":"Berglunds snabbköp",
  "City":"Luleå",
  "Country":"Sweden"
 },
 {
  "Name":"Centro comercial Moctezuma",
  "City":"México D.F.",
  "Country":"Mexico"
 },
 {
  "Name":"Ernst Handel",
  "City":"Graz",
  "Country":"Austria"
 },
 {
  "Name":"FISSA Fabrica Inter. Salchichas S.A.",
  "City":"Madrid",
  "Country":"Spain"
 },
 {
  "Name":"Galería del gastrónomo",
  "City":"Barcelona",
  "Country":"Spain"
 },
 {
  "Name":"Island Trading",
  "City":"Cowes",
  "Country":"UK"
 },
 {
  "Name":"Königlich Essen",
  "City":"Brandenburg",
  "Country":"Germany"
 },
 {
  "Name":"Laughing Bacchus Wine Cellars",
  "City":"Vancouver",
  "Country":"Canada"
 },
 {
  "Name":"Magazzini Alimentari Riuniti",
  "City":"Bergamo",
  "Country":"Italy"
 },
 {
  "Name":"North/South",
  "City":"London",
  "Country":"UK"
 },
 {
  "Name":"Paris spécialités",
  "City":"Paris",
  "Country":"France"
 },
 {
  "Name":"Rattlesnake Canyon Grocery",
  "City":"Albuquerque",
  "Country":"USA"
 },
 {
  "Name":"Simons bistro",
  "City":"København",
  "Country":"Denmark"
 },
 {
  "Name":"The Big Cheese",
  "City":"Portland",
  "Country":"USA"
 },
 {
  "Name":"Vaffeljernet",
  "City":"Århus",
  "Country":"Denmark"
 },
 {
  "Name":"Wolski Zajazd",
  "City":"Warszawa",
  "Country":"Poland"
 }
]

JSON 是以一個 KEY : VALUE 的形式存在著,好處是簡單易懂資料少、節省頻寬,所以成為大家很喜歡的一種交換格式。
那怎麼使用呢?

client.newCall(request).enqueue(object : Callback {  
 override fun onFailure(request: Request?, exception: IOException?) {}  

 override fun onResponse(response: Response) {  
  try {  
   val responseData = response.body().string()  
   val json = JSONObject(responseData)  
   val owner = json.getString("name")  
  } catch (e: JSONException) {}  
 }  
})

一樣是從 Response 物件回來的 body 取得字串,再透過原生的JSONObject 去存取,根據 key 把 value 取出來。
如何使用Gson 這篇文章中,我們可以透過比較方便的方法去解析 Json 字串,因此,使用 Gson 會是一種比較好的做法。
首先,我們將 Gson 的 library 加入到 build.gradle。

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

接著建立一個 Json 物件的類別,這邊可以先宣告好變數以後,透過Android Studio 的自動生成完成。

class JsonData {  
 @SerializedName("Name")  
 var name: String? = null  
 @SerializedName("City")  
 var city: String? = null  
 @SerializedName("Country")  
 var country: String? = null  
}

接著我們一樣從 Response 接回來以後,取出 body 再透過 Gson 轉成物件。

val request = Request.Builder()  
 .url("https://raw.githubusercontent.com/givemepassxd999/okhttp_demo/master/app/src/main/res/sample.json")  
 .build()  
client.newCall(request).enqueue(object : Callback {  
 override fun onFailure(request: Request, e: IOException) {  
  runOnUiThread { text.text = e.message }  
 }  

 @Throws(IOException::class)  
 override fun onResponse(response: Response) {  
  val resStr = response.body().string()  
  val jsonData = Gson().fromJson<List<JsonData>>(resStr, object : TypeToken<List<JsonData>>() {}.type)  
  runOnUiThread {  
   val sb = StringBuffer()  
   for (json in jsonData) {  
    sb.append("name:")  
    sb.append(json.name)  
    sb.append("\n")  
    sb.append("city:")  
    sb.append(json.city)  
    sb.append("\n")  
    sb.append("country:")  
    sb.append(json.country)  
    sb.append("\n")  
   }  
   text.text = sb.toString()  
  }  
 }  
})

這樣一來就可以看到這樣的畫面。


這樣就是一個簡單操作 OkHttp 的例子了。