上一章中,我们通过利用Handler和封装Volley,实现了自动化网络请求处理,但是其中还是有缺陷:
- Handler可能导致内存泄露
- 请求过程中显示的对话框太丑
- 网络请求结果返回的状态码没统一处理
这一章就来搞定这些问题。
Handler优化
首先,我以前也是按照上一章的样子使用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 extends Handler {
private WeakReference mTarget;
public MyHandler(T t) {
mTarget = new WeakReference(t);
}
@Override
public void handleMessage(Message msg) {
T target = mTarget.get();
if (target != null) {
target.onHandleMessage(msg);
}
}
}
MyHandler中采用泛型的好处在于,无论是Activity还是Fragment等,只要实现了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 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 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优化已经完成。
返回码统一处理
首先,我们看一下上一章请求接口返回的结果
现在我们将参数改为一个不合法的,可以看到返回错误。
看到这里,应该能清楚了,当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
-
1
无效IP地址
-
101
服务器错误
-
102
用户未登录
-
103
商品已下架
在里面添加了几项该接口不会返回的数据,仅作为参考。其实这里也可以为item加上属性,用于判断toast内容是xml中data项配置的,还是返回的data字段里面的。代码都不难,通过xml解析即可完成,我这里就不重点讲了。
通过配置实现错误处理
如果仅仅是通过校验返回的code来弹出对应Toast,功能太单一,而且有的后台请求是不需要对错误进行处理的,所以还记得一开始网络请求就多了一个Bundle类型的参数吗?这个参数不参与网络流程,但是对于请求结束后的配置相当重要。接下来新建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 mErrorMap = null;
/**
* 网络请求的bundle参数分析如下
* ignoreError (boolean,默认false),是否忽略所有errorCode,如后台调用
* ignoreToastErrorCode (ArrayList),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 ignoreList = bundle.getStringArrayList("ignoreToastErrorCode");
if (ignoreList != null && !ignoreList.isEmpty()) {
if (ignoreList.contains(errorCode)) {
return true;
}
}
}
return false;
}
}
其中ErrorMap的键值对就是code-data键值对。
替换系统ToastUtil
由于Android系统原生Toast有一个特点,如果你界面上有一个Button,每点击一次,则执行一次
Toast.makeText(MainActivity.this, "toast content", Toast.LENGTH_SHORT).show();
那么当用户连续点击的时候,就会一直重复弹出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,如果是连续几次操作,那么后面的消息会覆盖前面的内容,而不是像以前一样,等待前面的Toast结束,再重新弹出后续Toast。
合理打印log
这里穿插一下,我们应当在debug状态下打印出请求返回的信息,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中也可以,但推荐配置类的参数单独建文件,各司其职)。
demo
调整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 getErrorCodeMessageMap() throws IOException, XmlPullParserException {
HashMap 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数据,然后将其赋予NetworkError类的全局变量。同时,在MyApplication类onCreate方法中调用ErrorCodeParser.init()。
@Override
public void onCreate() {
super.onCreate();
mInstance = this;
mRequestQueue = Volley.newRequestQueue(this);
ErrorCodeParser.init();
}
完成错误处理
文章开头就给出正确和错误两种返回值,在这里,我们就认为code为0代表成功,其他的code表示失败,同时向handler发出错误码为-1的消息。那么现在继续来修改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中定义参数
public static final int NET_ERROR_CUSTOM = -1;
并且,为了更合理的理解,将原本RequestHandler类中定义的NET_ERROR_VOLLEY也移到NetworkError中。
最后,验证一下
onHandleMessage方法中增加一个case
case NetworkError.NET_ERROR_CUSTOM:
mTvCountry.setText("获取请求失败");
break;
修改loading框
目前我们采用的的加载框比较丑陋,在这里我们将定义自己的对话框。
获取素材
首先,我们的素材从哪里来呢?在这里给大家安利两个网站:
- http://fontawesome.io/cheatsheet/
我们可以在这个网站上找到图标素材,比如我们要找的,就是fa-spinner类型。
- http://fa2png.io/
将素材转换为png图片。访问网站,FA2PNG,输入spinner,你会发现有很多类似的,我们选择icomoon-spinner2这个素材。
右边是预览图,左边是选项,前景色(这里填入默认的#0064ff),背景色(我们选透明),图片尺寸,以及margin尺寸。
点击Generate后,即可生成图片。我们这个项目中,直接Download即可。
将图片改名为icon_loading_rotate.png,我们的loading框就做成一个转圈的动画,接下来我们来实现该对话框。
将得到的icon_loading_rotate.png放入drawable-xxhdpi目录下,新建res/anim/loading_rotate.xml
新建布局文件dialog_loading.xml
新建res/drawable/bg_dialog_loading.xml
res/values/styles.xml文件中添加:
因为我们目前使用的是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);
}
/**
* 子类重写该方法,即可创建样式相同的对话框。
* @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已经完,代码非常简单,这里不多解释,其中具体代码有疑问的应该都可以百度到,或者也可以直接问我。
接下来,要将我们自定义的对话框用到前面的网络流程中,只需将RequestHandler中ProgressDialog
改为LoadingDialog
。
测试一下,效果已经有了,我贴一张截图,不是动态的。
好了,本章到此结束。下一章的内容是:使目前的框架支持HTTPS安全请求,附带利用现在的对话框模板,创建一个消息框。
另,这些文章是边整理边写,如果有混乱的,欢迎指正,后续会优化改进,最后会将代码上传至github,有兴趣的可以去逛逛。捂脸,逃~