# Android Develope 5 - okhttp3 ft. AsyncTask - 學習J筆記 相信寫 Android app 的很多人應該都會用到 okhttp 這個第三方工具包,因為在我們進行聯網的時候用 java jdk 的 httpClient 是會出很多錯誤的。 我前面已經有介紹過 AsyncTask 了, okhttp3 則是有相當多網路上的教程,我自己是不太熟聯網那塊,能用就好所以我就不多作介紹。 這次主要講解一下如果想要在 UI 上面跟聯網做一些互動,然後不想自己寫 Hanlder/thread 的時候,可以怎麼搭配 okhttp 跟 AsyncTask ## 架構 簡單來說,我會使用 AsyncTask 直接包覆 okhttp 的聯網,再依靠回傳的數據來進行 UI 更新。 以下可以直接上程式碼(我這邊就不去介紹 API server 怎麼寫,單純實作 Client 端;實作的功能為登入功能 ```java= private class Login extends AsyncTask<String,String,String>{ private static final String TASK_PROGRESS = "Task progress" ; @Override protected void onPreExecute() { callLoginDialog(); Log.i(TAG,"Login..."); publishProgress("Login Prepare"); } @Override protected String doInBackground(String... params) { String username = params[0]; String password = params[1]; final String[] responseResult = {""}; Thread thread = Thread.currentThread(); publishProgress("Login.."); /* 連線設定 */ ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.COMPATIBLE_TLS) .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0) .allEnabledCipherSuites() .build();//解决在Android5.0版本以下https无法访问 ConnectionSpec spec1 = new ConnectionSpec.Builder(ConnectionSpec.CLEARTEXT).build();//兼容http接口 OkHttpClient client = new OkHttpClient().newBuilder().connectionSpecs(Arrays.asList(spec, spec1)) .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) .build(); /*建立 String 格式的 RsquestBody*/ MediaType JSON = MediaType.parse("application/json"); JSONObject json = new JSONObject(); try{ json.put("userId",username); json.put("password",password); }catch (Exception e){ Log.i(TAG,"json: "+e.getMessage()); } final RequestBody requestBody = RequestBody.create(JSON, String.valueOf(json)); /*Send Request*/ Request request = new Request.Builder() .url("http://[IP_ADDRESS]/[API_PATH]")//要送去的網址ser .post(requestBody) .addHeader("Content-Type","application/json") .build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(@NotNull Call call, @NotNull IOException e) {//這個 Func 主要是如果連線失敗的時候進入 Log.i(TAG,"HttpRequestError " + e.getMessage()); responseResult[0] = "error"; } @Override public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {//連線只要成功就會進這,所以 API 回傳的 invalid 也會在這 String responseString = response.body().string(); if(responseString.contains("error")){ responseResult[0] = "failed"; } else if(responseString.contains("access_token")){ responseResult[0] = "success"; String string = responseString .replace("{","") .replace("}","") .replace("\"",""); String[] res = string.split(","); for (String re : res) { String[] temp = re.split(":"); loginData.put(temp[0], temp[1]); } }else{ responseResult[0] = "unexpected"; } Log.i(TAG,"Login Done"); } }); /* 這裡很重要 */ int timerCount = 0; while(responseResult[0].equals("")){ if(timerCount>=20){ responseResult[0]="timeout"; } else{ timerCount++; } try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } /* 這裡很重要 */ return responseResult[0]; } @Override protected void onProgressUpdate(String... values) { Log.i(TASK_PROGRESS,values[0]); final TextView textView = LoginActivity.this.loginDialog.findViewById(R.id.textView_loginProgress); textView.setText(values[0]); } @Override protected void onPostExecute(String result) { Log.i(TAG,"Login prepare done"); if(!result.isEmpty()){ //TODO: Deal with every return msg, include wrong msg Log.i(TAG,"Login Result: "+ result); if(result.equals("success")){ Log.i(TAG,"登入成功"); endLogin(); //TODO: 登入成功後要做的事情... } else if(result.equals("fail")){ Log.i(TAG,"登入失敗"); //TODO: 登入失敗後要做的事情... endLogin(); } else if(result.equals("unexpected")){ Log.i(TAG,"非預期異常"); //TODO: 登入異常後要做的事情... endLogin(); } else if(result.equals("timeout")){ Log.i(TAG,"連線超時"); //TODO: 登入失敗後要做的事情... //因為我自己是有設計 reLogin 的功能,所以這邊我只關閉 AsyncTask 線程,但是其他事情不做 } else { Log.i(TAG,"連線失敗"); callWarningDialog("連線失敗"); endLogin(); } } else{ Log.i(TAG_ERROR,"Nothing Return"); callWarningDialog("登入動作\n無效"); endLogin(); } } private void endLogin(){ loginTask.cancel(true); //可能有設計一些 UI 要關閉 } @Override protected void onCancelled() { Log.i(TAG,"Login Dismissed"); } } ``` ## 源碼解說 有兩個比較重要的點,首先看第一個是在程式碼 54 行的地方,也就是 onResponse 的方法 因為我這支程式沒有引入解析 Json 的第三方工具包,所以我將 Response.body() 直接解析為 String 請看我的用法: ```java= String responseString = response.body().string(); ``` 在這裡要注意的是 1. string() 這個方法是將 response.body() 內容轉成 String 的型別,如果使用 response.body().toString,則只會將表面的 byteValue show出來,並不會轉碼成 String 型別的物件\ 2. string() 這個方法會切斷 I/O-Stream,只能呼叫一次其設計原理如下: > 在實際開發中,響應主體 RessponseBody 持有的資源可能會很大,所以 OkHttp 並不會將其直接儲存到記憶體中,只是持有資料流連線。只有當我們需要時,才會從伺服器獲取資料並返回。同時,考慮到應用重複讀取資料的可能性很小,所以將其設計為一次性流(one-shot),讀取後即 "關閉並釋放資源"。 第二個點則是在程式碼 78-91 行的地方 ```java= int timerCount = 0; while(responseResult[0].equals("")){ if(timerCount>=20){ responseResult[0]="timeout"; } else{ timerCount++; } try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } ``` 這裡為甚麼要這樣寫呢,是因為 okhttp POST 方法是透過 callback 去完成的,也就是說我們在 AsyncTask 這個異步線程當中再開了一個異步線程,如果這裡沒有讓 AsyncTask 的線程 sleep() 的話,那只要沒有再瞬間完成回傳 result 的話,每次傳出去的 result 就都會是預設值(現在是 null) 因此我們才要設計一個,如果當下紀錄的 result 為 "" 的話,AsyncTask 線程就睡 0.5 秒,當然這些都是可以自己設定的數字,重點是要保證邏輯的完整性,讓回傳結果之前和之後的判斷能有明確的分水嶺 ## 結語 以上就是 okhttp3 跟 AsyncTask 在 Android 當中實作的小範例,本次篇幅不多,大多都是程式碼,主要是這一塊踩到的坑不多,希望能夠幫助看到這篇文章的朋友 2021/10/26 ###### tags: `Android` `學習計畫` `AsyncTask` `okhttp3` `httpClient`