目前Android常用的网络请求框架有很多,比如Volley和Rettofit,功能强大且调用方便,学习这些框架的使用还算简单。不过只会调用这些API,其实跟学习Android系统框架层自带的API是一样的,无非是多用几次就熟悉了。API会的再多,对自己的开发水平的提高作用甚少。这就跟学英语类似,多学会几个API,就跟多学几个单词一样,单词是学不完的,而且学而不用,很快就会忘掉。
重点还是培养自己的思维方式,学习利用各种设计模式和编程原则,如何搭建一个框架,如何封装,如何解耦合,如何给用户留下方便易用的API来供调用。
这两天跟着stay的一个项目学习,尝试着搭建一个网络请求框架出来,项目名称我定为SimpleHttp。这篇文章对开发思路和过程做一个简单的整理,源码也跟随进度上传到Github,每次更新对应着不同的tag,tag名称为版本号,方便下载查看。
设计方式:在对一个框架的架构进行设计时,我的理解是应该遵循自顶向下的原则,要事先设计好有那些功能模块、要预留哪些API供开发者调用、要有哪些类、哪些方法,并设计出UML类图作为指导。
不过作为经验不足的程序员来说,架构的设计难度还是挺大的,也不知道应该从何做起。所以这个项目使用的方法是:先实现最基本的功能,然后再一步步进行封装、功能的扩展以及优化。
封装HttpUrlConnection
1、首先新建一个HttpUrlConnectionUtil
类,先分别封装实现最常用的GET
、POST
方法,其它的例如PUT
、DELETE
暂时不实现。
2、第一层封装:我们发现,在用GET
、POST
方法进行Http请求时,方法中需要输入许多参数,包括请求行(请求方法、URL)、头(Header)、体(请求参数)。GET
与POST
的区别就是,GET
方法的请求参数是写在URL里的,POST
的参数是写在请求体中的,然后与url直接拼接在一起。
我们发现,调用这两个方法有时需要输入多个参数,这样调用起来感觉很不友好,代码看起来也乱,所以我们可以想到将这些参数都封装在一个类中,这里就新建一个Request
类来封装这些请求参数,如下:
public enum RequestMethod{GET,POST,PUT,DELETE}
public RequestMethod method;
public String url;
public Map<String,String> header;
public String content;
为了测试方便,暂时先将成员变量都设置成public的。
3、封转完请求参数后,我们就可以直接调用SimpleHttp.get()
或SimpleHttp.post()
方法来进行Http请求了。但是问题也很明显,因为除了GET
和POST
,我们还有其它方法没实现呢,这样不仅暴露的API多,开发人员需要记住的API也多,所以这里我们还需要做进一步的封装。
所以我们在HttpUrlConnectionUtil
类中新建一个execute()
方法,来统一处理请求,通过一个分支判断来实现,如下:
public static String execute(Request request) throws IOException {
switch (request.method){
case GET:
return get(request);
case POST:
return post(request);
//TODO:后面还可以添加其它请求方法
}
return null;
}
做到这一步我们已经对Http请求做了一个基础的封装,当然还有很多瑕疵,这个就需要仔细考虑一些细节问题了。这里主要是学习一下整体的框架搭建的思路,所以不再深究一些细节问题。
实现将请求切换到子线程
Android系统要求,网络请求不能在主线程中进行操作,必须切换到子线程,所以这里我们要考虑如何实现在子线程中进行操作。Android线程间的通信也是一个非常重要的模块,主角是Handler,不过让我们自己用Handler来实现线程间的通信还是很麻烦的,那又是一个需要深入学习的功能模块,所以我们这里借用系统封装好的框架AsyncTask来实现。
1、我们新建一个RequestTask
类,并继承AsyncTask
,然后把HttpUrlConnectionUtil
类中的execute()
方法放到doInBackground()
方法中去执行即可。
@Override
protected Object doInBackground(Object[] params) {
try {
return HttpUrlConnectionUtil.execute(request);
} catch (IOException e) {
return e;
}
}
2、执行结束后,要如何获取请求返回的结果呢?如果熟悉AsyncTask
,我们知道子线程的执行操作在doInbackGround()
方法中执行,而它返回的数据会传给onPostExecute()
方法,所以我们可以在这个方法中获取返回数据。
但问题是当onPostExecute()
方法获得这个返回的数据后,还要将结果传给程序的最上层,就是在我们写的测试方法中取得这个数据,这个要怎么做呢?答案是使用回调。这里大体介绍一下回调的实现,具体可以参考代码:
1)首先建立一个回调接口ICallback
,该接口有两个回调方法OnSuccess
和OnFailure
:
public interface ICallback {
void onSuccess(String result);
void onFailure(Exception e);
}
2)在Request
类中设置一个Callback方法:
public ICallback iCallBack;
public void setCallback(ICallback iCallback) {
this.iCallBack = iCallback;
}
3)在OnPostExecute()
方法中调用ICallback
的两个方法,并传入在doInBackground()
中得到的结果:
@Override
protected void onPostExecute(Object o) {
super.onPostExecute(o);
if(o instanceof Exception){
request.iCallBack.onFailure((Exception) o);
}else {
request.iCallBack.onSuccess((String) o);
}
}
4)在我们的测试方法中调用Callback方法,得到结果:
request.setCallback(new ICallback() {
@Override
public void onSuccess(String result) {
Log.e("Test result3:",result);
}
@Override
public void onFailure(Exception e) {
e.printStackTrace();
}
});
切换到子线程的操作就基本完成了。
处理返回的数据,实现解析Json数据
之前统一返回数据的都是String类型,这很明显不能满足需求,比如还有常用的Json数据、文件等等,这节就先实现Json相关的功能。
1、在之前的实现当中,我们的ICallback
接口中的onSuccess()
方法里的参数是String格式的,所以这就导致我们只能返回String格式的数据,如果我们想返回其它的任意我们想要的格式(比如Json、Xml)要怎么做呢?答案是使用泛型,也就是在ICallback
接口中使用泛型:
public interface ICallback<T> {
void onSuccess(T result);
void onFailure(Exception e);
T parse(HttpURLConnection conn) throws Exception;
}
这样一来,接口当中的T
我们声明为什么格式,那么Onsuccess()
参数中的T
就是什么格式的。
2、当我们能够接收Json数据了,拿到Json数据后,我们还要进行解析的操作,例如将Json解析成我们自定义的JavaBean,不同的数据格式对应不同的解析操作,这一步要在哪里完成呢?
先看我们之前在只接收String格式的数据时是在哪里做的,之前是直接在HttpURLConnection
类中返回String结果。但现在数据类型多了,我们既要返回不同的数据类型,又要对不同的类型进行相应的解析。但是我们不可能将这些操作都放在HttpURLConnection
类中,因为根据单一职责原则,这个类只负责请求,接收和解析操作就放到别的类中去吧,那如何来做呢?
之前这个类返回的格式是String,现在返回的格式有多种,但是一个方法必须要有一个统一的返回格式吧,所以我们可以考虑提取get()
和post()
方法中的共性,来达到返回相同数据格式的目的。查看代码就很明了了,这两个方法的共性就是都会先建立connection,那么我们就返回connection,也就是HttpUrlConnection
格式,把接收数据的操作移到别的类中。这里我们移到了Callback
类中,它实现了ICallback
接口:
public abstract class Callback<T> implements ICallback<T> {
@Override
public T parse(HttpURLConnection conn) throws Exception {
//发起请求
conn.connect();
//获取结果
InputStream in = conn.getInputStream();
InputStreamReader isReader = new InputStreamReader(in);//将字节转成字符,适配器模式
BufferedReader reader = new BufferedReader(isReader);
StringBuilder result = new StringBuilder();
String line;
while((line = reader.readLine()) != null){
result.append(line);
}
in.close();
//返回结果
return parseResult(result.toString());
}
3、接收数据后,还要针对不同的数据格式进行不同的解析操作,格式不同,解析不同。我们自然想到,针对不同的格式,我们可以通过继承Callback
类,在子类中实现不同的解析,例如我们这里就实现了一个JsonCallback
子类来对Json数据进行解析:
public abstract class JsonCallback<T> extends Callback<T> {
@Override
protected T parseResult(String result) throws Exception {
JSONObject json = new JSONObject(result);
JSONObject data = json.optJSONObject("data");
Gson gson = new Gson();
Type type = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
return gson.fromJson(data.toString(), type);
}
}
这一小节整理的有些啰嗦,也没有讲很清楚,还是要多思考,仔细想想应该如何来实现。现在发现,要把自己理解的东西讲明白,也是一件不容易的事,更何况自己的理解也不是很到位,只能是多实践、多总结吧。
实现文件下载和进度更新
实现文件下载功能与实现返回Json或其它数据格式类似,就是实现一个FileCallback子类来返回相应的结果。
不过文件下载的过程就是一个接收返回数据的过程,数据接收完了,文件也就下载完了,它不需要返回什么结果,更不需要解析。所以我们这里返回一个path(下载路径)就可以了,要不然没什么可返回的。
这里整理一下大体过程,具体参考代码。
1、首先我们要在Callback中设置一个setPath()
方法,因为我们要传入一个下载路径吧。
2、然后在接收数据的方法中做一个判断,判断path是否为空,如果为空,则是请求的其它格式的数据,则做相应的操作。如果非空则是下载文件,则执行下载操作。
3、实现进度更新,这里也用到了回调,注意这里要给是否需要进度更新加一个判断,因为如果不是下载文件,是不需要这个功能的。具体参考代码:V1.5,因为这个功能是后来添加上的,是在添加异常处理之后,所以对应代码版本为V1.5。
异常处理
1、先看都有哪些地方可能会出现异常,需要处理:
1)在HttpUrlConnectionUtil中请求Http时,我们想返回status,要如何拿到呢?
2)如何处理TimeOut,出现TimeOut的可能的地方:请求时、解析数据时。(暂时还没实现)
2、需要返回哪些数据
1)status
2)ResponseMessage
3、如何来做?
1)出现异常也就是Exception,服务器会返回status(200、403等等),但是Exception里没有status,所以我们可以考虑自己封装一个HttpException,在里面加入status,这样我们就可以i在回调的时候取得这个状态码了。
2)将所有的Exception都替换成我们自定义的HttpException。
取消Http请求的实现
待更……
网友评论