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 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优化已经完成。

返回码统一处理

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

Android笔记 (5): 封装Volley实现自动化网络处理(中)_第1张图片
请求参数与返回结果

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

Android笔记 (5): 封装Volley实现自动化网络处理(中)_第2张图片
请求错误的参数

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

最后,验证一下

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

onHandleMessage方法中增加一个case

case NetworkError.NET_ERROR_CUSTOM:
    mTvCountry.setText("获取请求失败");
    break;
Android笔记 (5): 封装Volley实现自动化网络处理(中)_第3张图片
onHandleMessage方法
Android笔记 (5): 封装Volley实现自动化网络处理(中)_第4张图片
效果图

修改loading框

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

获取素材

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

  • http://fontawesome.io/cheatsheet/
Android笔记 (5): 封装Volley实现自动化网络处理(中)_第5张图片
cheatsheet

素材

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


fa-spinner
  • http://fa2png.io/

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


Android笔记 (5): 封装Volley实现自动化网络处理(中)_第6张图片
下拉列表中有很多选项

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


Android笔记 (5): 封装Volley实现自动化网络处理(中)_第7张图片
效果图

点击Generate后,即可生成图片。我们这个项目中,直接Download即可。
Android笔记 (5): 封装Volley实现自动化网络处理(中)_第8张图片
生成图片

icon_loading_rotate.png

将图片改名为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

Android笔记 (5): 封装Volley实现自动化网络处理(中)_第9张图片
修改前
Android笔记 (5): 封装Volley实现自动化网络处理(中)_第10张图片
修改后

测试一下,效果已经有了,我贴一张截图,不是动态的。

Android笔记 (5): 封装Volley实现自动化网络处理(中)_第11张图片
效果图

好了,本章到此结束。下一章的内容是:使目前的框架支持HTTPS安全请求,附带利用现在的对话框模板,创建一个消息框。
另,这些文章是边整理边写,如果有混乱的,欢迎指正,后续会优化改进,最后会将代码上传至github,有兴趣的可以去逛逛。捂脸,逃~


后续再会

你可能感兴趣的:(Android笔记 (5): 封装Volley实现自动化网络处理(中))