上一章中,我們通過利用Handler和封裝Volley,實現(xiàn)了自動化網(wǎng)絡請求處理,但是其中還是有缺陷:
- Handler可能導致內存泄露
- 請求過程中顯示的對話框太丑
- 網(wǎng)絡請求結果返回的狀態(tài)碼沒統(tǒng)一處理
這一章就來搞定這些問題。
Handler優(yōu)化
首先,我以前也是按照上一章的樣子使用Hanlder的,也正因此踩過這個坑,所以這里特別提出來。Android Lint會給這樣的用法給出提示:
In Android, Handler classes should be static or leaks might occur.
至于為什么會造成內存泄露,以及解決思路,請參照下文。
Android中使用Handler造成內存泄露的分析和解決
接下來我們動手解決這個問題,先新建com.joyin.volleydemo.utils.hander
包,在下面建IHandleMessage.java
及MyHandler.java
兩個文件。
IHandleMessage.java
package com.joyin.volleydemo.utils.hander;
import android.os.Message;
/**
* Created by joyin on 16-4-3.
*/
public interface IHandleMessage {
void onHandleMessage(Message message);
}
MyHandler.java
package com.joyin.volleydemo.utils.hander;
import android.os.Handler;
import android.os.Message;
import java.lang.ref.WeakReference;
/**
* Created by joyin on 16-4-3.
*/
public class MyHandler<T extends IHandleMessage> extends Handler {
private WeakReference<T> mTarget;
public MyHandler(T t) {
mTarget = new WeakReference<T>(t);
}
@Override
public void handleMessage(Message msg) {
T target = mTarget.get();
if (target != null) {
target.onHandleMessage(msg);
}
}
}
MyHandler中采用泛型的好處在于,無論是Activity還是Fragment等,只要實現(xiàn)了IHandleMessage接口,都可以實例化MyHandler對象來使用,在handleMessage()中也采取了保護措施。
接下來我們新建com.joyin.volleydemo.activity.BaseActivity類。
package com.joyin.volleydemo.activity;
import android.os.Bundle;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import com.joyin.volleydemo.utils.hander.IHandleMessage;
import com.joyin.volleydemo.utils.hander.MyHandler;
/**
* Created by joyin on 16-4-3.
*/
abstract public class BaseActivity extends AppCompatActivity implements IHandleMessage {
public MyHandler<BaseActivity> mHandler;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler = new MyHandler<>(this);
}
@Override
public void onHandleMessage(Message msg) {
}
}
修改MainActivity繼承自BaseActivity,并且刪除mHandler的定義,將原本handleMessage中的代碼移到onHandleMessage中,最終MainActivity的代碼如下:
package com.joyin.volleydemo.activity;
import android.os.Bundle;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;
import com.alibaba.fastjson.JSON;
import com.android.volley.Request;
import com.joyin.volleydemo.R;
import com.joyin.volleydemo.data.api.IpInfo;
import com.joyin.volleydemo.utils.network.RequestHandler;
import java.util.HashMap;
import java.util.Map;
public class MainActivity extends BaseActivity {
TextView mTvCountry, mTvCountryId, mTvIP;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
initData();
}
private void initViews() {
mTvCountry = (TextView) findViewById(R.id.tv_country);
mTvCountryId = (TextView) findViewById(R.id.tv_country_id);
mTvIP = (TextView) findViewById(R.id.tv_ip);
}
private void initData() {
String url = "http://ip.taobao.com/service/getIpInfo.php";
Map<String, String> params = new HashMap<>();
params.put("ip", "21.22.11.33");
RequestHandler.addRequestWithDialog(Request.Method.GET, MainActivity.this, mHandler, RESULT_GET_IP_INFO, null, url, params, null);
}
private void setIpInfoToView(IpInfo ipInfo) {
mTvCountry.setText(ipInfo.getData().getCountry());
mTvCountryId.setText(ipInfo.getData().getCountry_id());
mTvIP.setText(ipInfo.getData().getIp());
}
private static final int RESULT_GET_IP_INFO = 101;
@Override
public void onHandleMessage(Message msg) {
switch (msg.what) {
case RESULT_GET_IP_INFO:
String result = (String) msg.obj;
Log.d("demo", result);
IpInfo ipInfo = JSON.parseObject(result, IpInfo.class);
setIpInfoToView(ipInfo);
break;
}
}
}
至此,Handler優(yōu)化已經(jīng)完成。
返回碼統(tǒng)一處理
首先,我們看一下上一章請求接口返回的結果
現(xiàn)在我們將參數(shù)改為一個不合法的,可以看到返回錯誤。
看到這里,應該能清楚了,當code是0的時候表示返回結果正確,code為1表示錯誤。這種情況,我們在界面上彈出Toast,內容為“無效IP地址”,那么就可以通過xml文件配置,文件名為:return_codes.xml,放在assets/configs目錄下。
XML配置
首先修改build.gradle文件,在android{}結構中加入如下代碼指定assets目錄:
sourceSets {
main {
assets.srcDirs = ['assets']
}
}
app/assets/configs/return_codes.xml
<?xml version="1.0" encoding="utf-8"?>
<return_data>
<item>
<code>1</code>
<data>無效IP地址</data>
</item>
<item>
<code>101</code>
<data>服務器錯誤</data>
</item>
<item>
<code>102</code>
<data>用戶未登錄</data>
</item>
<item>
<code>103</code>
<data>商品已下架</data>
</item>
</return_data>
在里面添加了幾項該接口不會返回的數(shù)據(jù),僅作為參考。其實這里也可以為item加上屬性,用于判斷toast內容是xml中data項配置的,還是返回的data字段里面的。代碼都不難,通過xml解析即可完成,我這里就不重點講了。
通過配置實現(xiàn)錯誤處理
如果僅僅是通過校驗返回的code來彈出對應Toast,功能太單一,而且有的后臺請求是不需要對錯誤進行處理的,所以還記得一開始網(wǎng)絡請求就多了一個Bundle類型的參數(shù)嗎?這個參數(shù)不參與網(wǎng)絡流程,但是對于請求結束后的配置相當重要。接下來新建com.joyin.volleydemo.utils.network.NetworkError類。
package com.joyin.volleydemo.utils.network;
import android.os.Bundle;
import com.alibaba.fastjson.JSONObject;
import com.joyin.volleydemo.utils.ui.ToastUtil;
import java.util.ArrayList;
import java.util.HashMap;
/**
* Created by joyin on 16-4-3.
*/
public class NetworkError {
public static HashMap<String, String> mErrorMap = null;
/**
* 網(wǎng)絡請求的bundle參數(shù)分析如下
* ignoreError (boolean,默認false),是否忽略所有errorCode,如后臺調用
* ignoreToastErrorCode (ArrayList<String>),list里面的errorCode會被忽略掉
*/
public static void error(String errorCode, JSONObject jsonObject, Bundle bundle) {
if (bundle != null) {
// ignoreError若為true,則忽略所有errorCode
if (bundle.getBoolean("ignoreError", false)) {
// 該請求無需錯誤處理
return;
}
}
if (mErrorMap == null) {
return;
}
if (!checkIgnoreCodes(bundle, errorCode)) {
parseDefaultErrorCode(errorCode, jsonObject);
}
}
/**
* 遍歷code,彈出對應錯誤信息Toast
*/
private static void parseDefaultErrorCode(String errorCode, JSONObject jsonObject) {
if (mErrorMap != null && mErrorMap.containsKey(errorCode)) {
ToastUtil.show(mErrorMap.get(errorCode));
return;
}
ToastUtil.show(jsonObject.toString());
}
/**
* 檢查該code是否需忽略
*
* @return 驗證是否通過
*/
private static boolean checkIgnoreCodes(Bundle bundle, String errorCode) {
if (bundle != null) {
// 若errorCode存在于該list,則由調用者自己處理
ArrayList<String> ignoreList = bundle.getStringArrayList("ignoreToastErrorCode");
if (ignoreList != null && !ignoreList.isEmpty()) {
if (ignoreList.contains(errorCode)) {
return true;
}
}
}
return false;
}
}
其中ErrorMap的鍵值對就是code-data鍵值對。
替換系統(tǒng)ToastUtil
由于Android系統(tǒng)原生Toast有一個特點,如果你界面上有一個Button,每點擊一次,則執(zhí)行一次
Toast.makeText(MainActivity.this, "toast content", Toast.LENGTH_SHORT).show();
那么當用戶連續(xù)點擊的時候,就會一直重復彈出Toast,這點相信大家都明白,用戶體驗很低。我們新建com.joyin.volleydemo.utils.ui.ToastUtil類。
package com.joyin.volleydemo.utils.ui;
import android.widget.Toast;
import com.joyin.volleydemo.app.MyApplication;
/**
* Created by joyin on 16-4-3.
*/
public class ToastUtil {
private ToastUtil() {
}
private static Toast mToast;
public static void show(int resId) {
show(MyApplication.getInstance().getString(resId));
}
public static void show(String msg) {
if (mToast == null) {
mToast = Toast.makeText(MyApplication.getInstance(), msg, Toast.LENGTH_SHORT);
} else {
mToast.setText(msg);
}
mToast.show();
}
}
通過這樣的方式彈出Toast,如果是連續(xù)幾次操作,那么后面的消息會覆蓋前面的內容,而不是像以前一樣,等待前面的Toast結束,再重新彈出后續(xù)Toast。
合理打印log
這里穿插一下,我們應當在debug狀態(tài)下打印出請求返回的信息,release版本安全性較高,不應打印這些log,新建com.joyin.volleydemo.utils.app.LogUtil類。
package com.joyin.volleydemo.utils.app;
import android.util.Log;
import com.joyin.volleydemo.BuildConfig;
import com.joyin.volleydemo.R;
import com.joyin.volleydemo.app.MyApplication;
/**
* Created by joyin on 16-4-3.
*/
public class LogUtil {
private LogUtil() {
}
public static final boolean DEBUG = BuildConfig.DEBUG;
public static final String TAG = MyApplication.getInstance().getString(R.string.config_logcat_tag);
public static void d(String msg) {
d(TAG, msg);
}
public static void d(String tag, String msg) {
if (DEBUG) {
Log.d(tag, msg);
}
}
public static void e(String msg) {
e(TAG, msg);
}
public static void e(String tag, String msg) {
if (DEBUG) {
Log.e(tag, msg);
}
}
public static void v(String msg) {
v(TAG, msg);
}
public static void v(String tag, String msg) {
if (DEBUG) {
Log.v(tag, msg);
}
}
public static void exception(String msg) {
e(msg);
}
}
在res/values/下新建configs.xml,將R.string.config_logcat_tag加入其中(直接添加在strings.xml中也可以,但推薦配置類的參數(shù)單獨建文件,各司其職)。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="config_logcat_tag">demo</string>
</resources>
調整RequestHandler類,onVolleyResponse方法里面加上
LogUtil.d(response);
解析xml
接下來回到正題,新建com.joyin.volleydemo.utils.parse.xml.ErrorCodeParser類。
package com.joyin.volleydemo.utils.parse.xml;
import android.text.TextUtils;
import android.util.Xml;
import com.joyin.volleydemo.app.MyApplication;
import com.joyin.volleydemo.utils.network.NetworkError;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
/**
* Created by joyin on 16-4-3.
*/
public class ErrorCodeParser {
public static void init() {
try {
NetworkError.mErrorMap = getErrorCodeMessageMap();
} catch (IOException e) {
e.printStackTrace();
} catch (XmlPullParserException e) {
e.printStackTrace();
}
}
private static HashMap<String, String> getErrorCodeMessageMap() throws IOException, XmlPullParserException {
HashMap<String, String> map = null;
XmlPullParser parser = Xml.newPullParser();
InputStream in = MyApplication.getInstance().getAssets().open("configs/return_codes.xml");
parser.setInput(in, "UTF-8");
int eventType = parser.getEventType();
String key = "";
String value = "";
while (eventType != XmlPullParser.END_DOCUMENT) {
String nodeName = parser.getName();
switch (eventType) {
case XmlPullParser.START_DOCUMENT:
map = new HashMap<>();
break;
case XmlPullParser.START_TAG:
if (nodeName.equals("code")) {
key = parser.nextText();
} else if (nodeName.equals("data")) {
value = parser.nextText();
}
break;
case XmlPullParser.END_TAG:
if (nodeName.equals("item") && !TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) {
map.put(key, value);
}
break;
}
eventType = parser.next();
}
return map;
}
}
代碼很簡單,就是解析錯誤信息xml數(shù)據(jù),然后將其賦予NetworkError類的全局變量。同時,在MyApplication類onCreate方法中調用ErrorCodeParser.init()。
@Override
public void onCreate() {
super.onCreate();
mInstance = this;
mRequestQueue = Volley.newRequestQueue(this);
ErrorCodeParser.init();
}
完成錯誤處理
文章開頭就給出正確和錯誤兩種返回值,在這里,我們就認為code為0代表成功,其他的code表示失敗,同時向handler發(fā)出錯誤碼為-1的消息。那么現(xiàn)在繼續(xù)來修改RequestHandler中onVolleyResponse方法的代碼。
private static void onVolleyResponse(String response, Handler handler, int what, Bundle bundle) {
LogUtil.d(response);
JSONObject json = JSON.parseObject(response);
if (json != null && json.containsKey("code")) {
int code = json.getIntValue("code");
if (code != 0) {
// 如果code不為0,則走錯誤處理流程
Message msg = handler.obtainMessage(NetworkError.NET_ERROR_CUSTOM);
msg.setData(bundle);
handler.sendMessage(msg);
NetworkError.error("" + code, json, bundle);
return;
}
}
Message msg = handler.obtainMessage(what, response);
msg.setData(bundle);
handler.sendMessage(msg);
}
同時在NetworkError中定義參數(shù)
public static final int NET_ERROR_CUSTOM = -1;
并且,為了更合理的理解,將原本RequestHandler類中定義的NET_ERROR_VOLLEY也移到NetworkError中。
最后,驗證一下
onHandleMessage方法中增加一個case
case NetworkError.NET_ERROR_CUSTOM:
mTvCountry.setText("獲取請求失敗");
break;
修改loading框
目前我們采用的的加載框比較丑陋,在這里我們將定義自己的對話框。
獲取素材
首先,我們的素材從哪里來呢?在這里給大家安利兩個網(wǎng)站:
我們可以在這個網(wǎng)站上找到圖標素材,比如我們要找的,就是fa-spinner類型。
將素材轉換為png圖片。訪問網(wǎng)站,F(xiàn)A2PNG,輸入spinner,你會發(fā)現(xiàn)有很多類似的,我們選擇icomoon-spinner2這個素材。
下拉列表中有很多選項
右邊是預覽圖,左邊是選項,前景色(這里填入默認的#0064ff),背景色(我們選透明),圖片尺寸,以及margin尺寸。
效果圖
點擊Generate后,即可生成圖片。我們這個項目中,直接Download即可。
生成圖片
icon_loading_rotate.png
將圖片改名為icon_loading_rotate.png,我們的loading框就做成一個轉圈的動畫,接下來我們來實現(xiàn)該對話框。
將得到的icon_loading_rotate.png放入drawable-xxhdpi目錄下,新建res/anim/loading_rotate.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<rotate
android:duration="1500"
android:fromDegrees="0"
android:interpolator="@android:anim/linear_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="-1"
android:repeatMode="restart"
android:toDegrees="+360" />
</set>
新建布局文件dialog_loading.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_dialog_loading"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp">
<ImageView
android:id="@+id/icon_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:src="@drawable/icon_loading_rotate" />
</LinearLayout>
新建res/drawable/bg_dialog_loading.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="5dp" />
<solid android:color="#88333333" />
</shape>
res/values/styles.xml文件中添加:
<style name="default_dialog" parent="android:style/Theme.Dialog">
<item name="android:windowFrame">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
因為我們目前使用的是loading框,而項目中往往還會有消息提示框等,所以我們將具體代碼抽象出來。新建com.joyin.volleydemo.view.dialog.BaseDialog.java
package com.joyin.volleydemo.view.dialog;
import android.app.Dialog;
import android.content.Context;
import android.view.View;
import com.joyin.volleydemo.R;
/**
* Created by joyin on 16-4-4.
*/
public abstract class BaseDialog {
public Dialog mDialog;
public BaseDialog(Context context) {
View view = getDefaultView(context);
mDialog = createDialog(context, view);
}
/**
* 子類重寫該方法,即可創(chuàng)建樣式相同的對話框。
* @param context
* @return
*/
protected abstract View getDefaultView(Context context);
private static Dialog createDialog(Context context, View v) {
Dialog dialog = new Dialog(context, R.style.default_dialog);
dialog.setCancelable(false);
dialog.setContentView(v);
return dialog;
}
public void show() {
if (mDialog != null) {
mDialog.show();
}
}
public void dismiss() {
if (mDialog != null) {
mDialog.dismiss();
}
}
}
新建com.joyin.volleydemo.view.dialog.LoadingDialog.java
package com.joyin.volleydemo.view.dialog;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import com.joyin.volleydemo.R;
/**
* Created by joyin on 16-4-4.
*/
public class LoadingDialog extends BaseDialog {
public LoadingDialog(Context context) {
super(context);
}
@Override
protected View getDefaultView(Context context) {
LayoutInflater inflater = LayoutInflater.from(context);
View v = inflater.inflate(R.layout.dialog_loading, null);
ImageView icon = (ImageView) v.findViewById(R.id.icon_loading);
Animation animation = AnimationUtils.loadAnimation(context, R.anim.loading_rotate);
icon.startAnimation(animation);
return v;
}
}
至此,自定義Dialog已經(jīng)完,代碼非常簡單,這里不多解釋,其中具體代碼有疑問的應該都可以百度到,或者也可以直接問我。
接下來,要將我們自定義的對話框用到前面的網(wǎng)絡流程中,只需將RequestHandler中ProgressDialog
改為LoadingDialog
。
測試一下,效果已經(jīng)有了,我貼一張截圖,不是動態(tài)的。
好了,本章到此結束。下一章的內容是:使目前的框架支持HTTPS安全請求,附帶利用現(xiàn)在的對話框模板,創(chuàng)建一個消息框。
另,這些文章是邊整理邊寫,如果有混亂的,歡迎指正,后續(xù)會優(yōu)化改進,最后會將代碼上傳至github,有興趣的可以去逛逛。捂臉,逃~