Android笔记 (5): 封装Volley实现自动化网络处理(中)

上一章中,我们通过利用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.javaMyHandler.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等,只要实现了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优化已经完成。

返回码统一处理

首先,我们看一下上一章请求接口返回的结果

请求参数与返回结果

现在我们将参数改为一个不合法的,可以看到返回错误。

请求错误的参数

看到这里,应该能清楚了,当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>

在里面添加了几项该接口不会返回的数据,仅作为参考。其实这里也可以为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<String, String> mErrorMap = null;

    /**
     * 网络请求的bundle参数分析如下
     * 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键值对。

替换系统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中也可以,但推荐配置类的参数单独建文件,各司其职)。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="config_logcat_tag">demo</string>
</resources>

调整RequestHandler类,onVolleyResponse方法里面加上

LogUtil.d(response);
onVolleyResponse

解析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数据,然后将其赋予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中。

最后,验证一下

修改ip参数为不合法的ip地址

onHandleMessage方法中增加一个case

case NetworkError.NET_ERROR_CUSTOM:
    mTvCountry.setText("获取请求失败");
    break;
onHandleMessage方法
效果图

修改loading框

目前我们采用的的加载框比较丑陋,在这里我们将定义自己的对话框。

获取素材

首先,我们的素材从哪里来呢?在这里给大家安利两个网站:

cheatsheet

素材

我们可以在这个网站上找到图标素材,比如我们要找的,就是fa-spinner类型。


fa-spinner

将素材转换为png图片。访问网站,FA2PNG,输入spinner,你会发现有很多类似的,我们选择icomoon-spinner2这个素材。


下拉列表中有很多选项

右边是预览图,左边是选项,前景色(这里填入默认的#0064ff),背景色(我们选透明),图片尺寸,以及margin尺寸。


效果图

点击Generate后,即可生成图片。我们这个项目中,直接Download即可。
生成图片

icon_loading_rotate.png

将图片改名为icon_loading_rotate.png,我们的loading框就做成一个转圈的动画,接下来我们来实现该对话框。
将得到的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);
    }

    /**
     * 子类重写该方法,即可创建样式相同的对话框。
     * @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,有兴趣的可以去逛逛。捂脸,逃~


后续再会
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容