發送網絡請求時我們寫大部分安卓項目時無法避免的一環,使用OkHttp庫可以很好的幫助我們封裝網絡請求的底層處理細節,更專注的完成實際業務需求。但是安卓不允許我們直接在UI線程運行網絡請求,因為網絡請求可能會阻塞UI的響應,因此我們只能開辟新的線程來處理網絡請求。那么怎么在網絡請求完成后在子線程中更新UI呢?下面我們就來討論一下幾種簡單的實現方式。
1. 使用AsyncTask + OkHttp同步請求
AsyncTask類是用來在子線程中異步處理一些耗時操作的一個工具類,我們先繼承AsyncTask類編寫一個處理自己業務需求的子類,然后在其中編寫邏輯代碼,最后在UI線程中啟動AsyncTask即可,如下代碼:
private class UserLoginTask extends AsyncTask<String, Void, LoginResult> {
/**
* 登錄參數
* @param params params[0] ==> username,
* params[1] ==> password
* @return
*/
@Override
protected LoginResult doInBackground(String... params) {
String username = params[0];
String password = params[1];
NetworkHelper networkHelper = NetworkHelper.getInstance();
try {
User user = networkHelper.login(username, password);
//login failed
if (user == null) {
Log.i(TAG, "Username and password do not match");
result.setStatus(ResultStatus.AUTH_ERROR);
} else {
Log.i(TAG, "Login success");
result.setStatus(ResultStatus.SUCCESS);
result.setUser(user);
}
} catch (IOException e) {
Log.e(TAG, "Login failed due to network error", e);
result.setStatus(ResultStatus.NETWORK_ERROR);
}
return result;
}
@Override
protected void onPostExecute(LoginResult loginResult) {
mProgressDialog.hide();
if (loginResult.getStatus() == ResultStatus.SUCCESS) {
// do login success task, update UI
...
} else if (loginResult.getStatus() == ResultStatus.NETWORK_ERROR){
Toast.makeText(getApplication(), R.string.error_network_fail, Toast.LENGTH_SHORT)
.show();
} else if (loginResult.getStatus() == ResultStatus.AUTH_ERROR) {
Toast.makeText(getApplication(), R.string.error_incorrect_password, Toast.LENGTH_SHORT)
.show();
}
}
}
以上代碼中UserLoginTask是LoginActivity的一個子類,其中最重要的有兩個方法:doInBackground(String... params)
和onPostExecute(LoginResult loginResult)
,前者的返回值會傳入后者的方法參數中。前者在子線程中執行,因此不可以在doInBackground中更新UI,否則會拋出異常,而后者是在主線程中執行的,所以相關UI操作可以在這里進行。
然后在Activity中為LoginButton設置監聽,在點擊時創建并啟動LoginTask:
mLoginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
login();
}
});
...
private void login() {
String username = mUsernameEditText.getText().toString();
String password = mPasswordEditText.getText().toString();
if (username.isEmpty() || password.isEmpty()) {
Toast.makeText(getApplication(), R.string.error_empty_username_or_password, Toast.LENGTH_SHORT)
.show();
return;
}
//如果登錄任務還未完成,防止創建重復的登錄任務
if (mLoginTask != null) {
return;
}
mProgressDialog.show();
mLoginTask = new UserLoginTask();
mLoginTask.execute(username, password);
}
其中最后一行代碼mLoginTask.execute(username, password)
就是在子線程中執行LoginTask中方法。
以上僅為創建子線程任務的部分,而其中networkHelper.login(username, password)
調用中才是真正執行OkHttp請求的地方,因為AsyncTask已經是子線程了,所以在發送OkHttp請求時就不需要使用異步請求,發送同步請求就可以了,下面給出其簡單代碼(以post請求為例):
public User login(String username, String password) throws IOException {
OkHttpClient client = new OkHttpClient();
String requestBody = "{\"username\": \"" + username + "\", \"password\": \"" + password + "\"}";
String res;
RequestBody body = RequestBody.create(JSON, requestBody);
Request request = new Request.Builder()
.url(baseUrl + path) //你的請求URL
.post(body)
.build();
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
res = response.body().string();
} else {
throw new IOException("Unexpected code " + response);
}
//用戶名或密碼錯誤,返回空字符串
if (res == null || res.trim().isEmpty()) {
return null;
}
return gson.fromJson(res, User.class);
}
OkHttp的使用非常簡單,創建client,創建request,然后調用client.newCall(request).execute()就可以得到response,然后對其進行處理即可,詳情可參考官方文檔,這里不再贅述。
2. 使用OkHttp的異步請求
異步OkHttp請求可以是我們不需要再編寫自己的AsyncTask了,OkHttp會自動在子線程中執行網絡請求,并在請求成功或失敗后回來調用相應的回調方法,如下:
public void asyncGetStudent(final Activity activity, int groupId, final NetworkCallback<User> callback) {
String authToken = getAuthToken(activity);
String path = "/group/" + groupId + "/students";
Request request = buildGetRequest(path, authToken);
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
callback.onGetFail(e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
onFailure(call, new IOException("Unexpected Code: " + response));
} else {
String responseJson = response.body().string();
User[] students = gson.fromJson(responseJson, User[].class);
callback.onGetSuccess(students);
}
}
});
}
異步請求與同步請求的主要區別在于調用clinet.newCall(request).enqueue(Callback)
而非execute()方法,然后在onFailure
和onResponse
中處理結果。
為了集中處理網絡請求,我們依然將所有網絡請求的代碼放在了NetworkHelper類中,然后在Activity中調用其中的方法,以上代碼中的NetworkCallback<T>回調接口如下:
public interface NetworkCallback<T> {
void onGetSuccess(T[] resultList);
void onGetFail(Exception ex);
}
該回調接口在網絡請求完成后調用,其實現定義在調用網絡請求的Activity或Fragment中:
protected void onCreate(Bundle savedInstance) {
NetworkHelper.getInstance().asyncGetStudent(this, groupId, new NetworkCallback<User>() {
@Override
public void onGetSuccess(User[] resultList) {
mStudents = resultList;
runOnUiThread(new Runnable() {
@Override
public void run() {
updateUI();
}
});
}
@Override
public void onGetFail(Exception ex) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(StudentListActivity.this, R.string.error_network_fail, Toast.LENGTH_SHORT)
.show();
}
});
}
}
在上面的代碼片段中,我們在拿到結果resultList后,沒有直接使用它來更新UI,而是調用了一個runOnUIThread(Runnable runable)方法,這是為什么呢?
因為回調方法的執行依然是在子線程中的,所以回調方法中依然不能更新UI!,這里我們使用一個簡單方便的調用runOnUiThread
來更新UI,該方法接受一個Runnable對象,只要在Runnable中更新UI就可以了。