仅需6步,教你轻易撕掉app开发框架的神秘面纱(4):网络模块的封装
程序框架确定了,还需要封装网络模块。
一个丰富多彩的APP少不了网络资源的支持,毕竟用户数据要存储,用户之间也要交互,用户行为要统计等等。
使用开源框架
俗话说得好,轮子多了路好走,我们不需要自己造轮子,拿来主义就行了。
android网络模块核心功能使用xUtils3开源框架来完成。
而iOS则使用AFNetWorking,别告诉我你没听说过AFNetworking。
xUtils3拥有4大功能:数据库,视图注解,网络,图片(支持webp)。
AFNetWorking则包含网络和图片2部分。
我们只需要用到其中的网络模块和图片缓存模块。
Model(Record)封装:
《App研发录》中强烈要求把后台返回的json数据转换成类实例Record(有些人喜欢称为model,即:MVC中的M,而个人习惯称之为Record,而Model的使用我更倾向于可共享可本地化的全局单例)类。在业务逻辑中使用的是这些类实例化后的对象。
这样做的好处有3个:
- 不易出错。JSONObject对象操作起来有点麻烦,比如:每次需要使用has方法来判断某些值是否存在,如果不判断,而这些值恰好不存在,则会崩溃。更重要的是,需要使用字符串来做键,写错了也是没有编译器提示的。
- 数据传递更为容易。页面间,对象间传递数据可直接传递Record对象,更加具有可读性,也更高效。如果传递JSONObject则需要再次解析。从而造成同一个数据多次解析。
- 代码更加规范。可以把Json转Record封装到网络层,从而在使用者看来,网络请求回来的数据就是Record。这样更容易让不同的程序员做更少的事情,从而写出尽量类似的代码。
为了达到上述目的,我们需要再次引入一个第三方库,来自Google的Gson,如何引入及如何调用请另行查询,它的作用就是把json字符串转换成本地类对象。
iOS则需要引入另一个第三方库,MJExtension。这个库作用同Android的Gson,但是相对android来说,它更加强大,更加易用。使用MJExtension的方法请见官方Demo。
然后我们需要建立一个基类BaseRecord来表示网络数据的基类,它是一个空的类,实现了Serializable这个接口,目的是让它可以通过Intent传递,也可以方便的本地化(把对象写入到硬盘)。
//android:
//BaseRecord.java
public class BaseRecord implements Serializable{}
//iOS:
//BaseRecord.h
@interface BaseRecord: NSObject
@end
//BaseRecord.m
@implements BaseRecord
@end
后续所有的表示服务端返回的数据都需要继承BaseRecord这个类,这样写在设计模式中对应的说法是:里氏替换。
至于具体的record如何写,如何使用Gson进行绑定,下面代码中有部分内容,更多细节请自行查询资料。
这里提供一个json自动转java类的网址作为参考。
ServerBinder的封装:
为了达到上述目的,让使用者用最简单的方法就能够获取到网络资源,我们需要封装一个类,ServerBinder。
ServerBinder是一个单例,它需要用户输入后台接口的名字后,然后输出一个对应的存储了所有返回的服务端数据的Record。
ServerBinder中需要这样一个方法:regist,表示注册某个接口,只有在ServerBinder中注册过的服务端接口,留下了必要信息,后续才能够调用。
我们需要分析一下服务端调用地址的构成,来决定此方法的传入参数:
服务端接口往往是这样的,http://xxx.com/api/user_info?id=1000
其中可变的部分为:
- http://xxx.com:表示服务器地址
- api:服务端入口
- user_info:接口名,
- ?后面表示参数。
这样,我们的regist函数包括5个参数:网址,服务端入口,接口名,接口类型(get还是post),还有返回的record的类型。此函数需要做到,把地址,入口,方法名,record类型 存储起来。存储的数据需以方法名为键。此方法全局只需调用一次。
以方法名为键的原因是:对于服务端来说,同一个方法名对应的数据格式是相同的。
我们还需要一个方法:call,来表示调用此接口,可以在任何需要网络数据的时候调用它。
call方法需要3个参数,方法名,参数列表,还有回调函数(实现为一个内部接口,供调用者实现,类似观察者模式,但是这个观察者寿命比较短,只能观察一次)。
用户调用call方法时,所需要的数据都有了。返回的数据需要在真正的服务端回调中处理,把json转成record,然后把结果交给上面说的观察者即可。
另外每次服务端数据返回,都会带有当前服务器时间,因此客户端需要做时间校正:令app客户端每次获取的时间都是服务器时间,避免用户修改设置里面的手机时间,导致app内时间错误。
好了,知道了上面的内容,我们就可以写一份完整的封装网络数据的类了。内容如下(下面代码仅是伪代码,使用时请自行调试)。
//android:
//ServerBinder.java
public class ServerBinder{private final static String TAG = "ServerBinder";private long timeOffset = 0;//服务器时间和本地时间的差值//单例private ServerBinder(){}private static ServerBinder sBinder = null;public sythornized ServerBinder getInstance(){if(sBinder == null){sBinder = new ServerBinder();}return sBinder;}//保存所有注册的数据,当然要保存了,不保存怎么调用?private HashMap<String, BindData> mBindDatas;//表示注册的服务端数据public static class BindData{public String addr;//服务端地址public String entry;//服务端代码入口public String ifaceName;//接口名public String ifaceType;//接口类型public Class <?> recordClass;//返回record类型}//服务端返回数据public static class ServerData{public BindData bindData;//注册数据,让你分辨是什么接口及参数public BaseRecord serverRecord;//服务端返回的数据public int status;//接口调用状态 status为1表示成功,为0表示失败public String message;//服务端返回的错误或提示信息}//客户端回调接口public interface ServerCallback{//status 表示网络请求状态,bindData表示当前请求相关参数,record表示返回数据public void onServerCallback(ServerData data);}//注册!!public void regist(String addr, String entry, String ifaceName, String ifaceType, Class<?> recordClass){//初始化BindDataBindData data = new BindData();data.addr = addr;data.entry = entry;data.ifaceName = ifaceName;data.recordClass = recordClass;data.ifaceType = ifaceType;//把数据存起来mBindDatas.put(entry, data);}//客户端调用接口,注意接口参数,params是一个字符串数组,后端是无类型的php,可以这样写,但是如果后端是java则需要修改。或者可以用json。public void call(String ifaceName, ServerCallback cb, String ...params){if(!mBindDatas.contains(ifaceName)){Log.e();return;}BindData bindData = mBindDatas.get(ifaceName);switch(bindData.ifaceType){case "get":get(bindData, params, cb);break;case "post":post(bindData,params, cb);break;case "download":download(bindData, params, cb);break;case "upload":upload(bindData,params, cb);break;}}/*假设服务端数据格式为:{"status": 1,//1表示正确 0表示错误"time":17383592394,"message": "一切正常","data":{//需要转换成record的部分}}*/private void handleResponse(BindData bindData, String jsonStr, ServerCallback cb){JSONObject jsonObj = new JSONObject(jsonStr);ServerData serverData = new ServerData();serverData.bindData = bindData;serverData.status = jsonObj.getInt("status");serverData.message = jsonObj.getString("message");if(serverData.status == 1){String data = jsonObj.getObject("data").toString();serverData.serverRecord = (BaseRecord)new Gson().fromJson(data, bindData.recordClass);}cb.onServerCallback(serverData);//时间校正if(jsonObj.contains("time")){long time = jsonObj.getLong("time");timeOffset = time - getLocalTime();}}public long getLocalTime(){return System.currentTimeMillis();//毫秒,注意时间单位的统一。}public long getServerTime(){return getLocalTime() + timeOffset;}// 下面就是真正调用接口了// 另外iOS版本的ServerBinder,除了下面的4个函数内容不一样之外,其余部分逻辑完全一致。// 只需要把java翻译成objective-c即可。public void get(BindData bindData, String[]params, ServerCallback cb){//...TODO 使用xutils接口获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置handleResponse(bindData, jsonStr, cb);}public void post(BindData bindData, String[]params, ServerCallback cb){//...TODO 使用xutils接口获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置handleResponse(bindData, jsonStr, cb);}public void download(BindData bindData, ServerCallback cb){//...TODO 使用xutils接口获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置handleResponse(bindData, jsonStr, cb);}public void upload(BindData bindData, String[]params, ServerCallback cb){//...TODO 使用xutils接口获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置handleResponse(bindData, jsonStr, cb);}
}
//ServerBinder.h
#import <Foundation/Foundation.h>//表示注册的服务端数据
@interface BindData : NSObject
@property (nonatomic, copy) NSString *addr;
@property (nonatomic, copy) NSString *entry;
@property (nonatomic, copy) NSString *ifaceName;
@property (nonatomic, copy) NSString *ifaceType;
@property (nonatomic, copy) Class recordClass;
@end//表示服务端返回数据
@interface ServerData : NSObject
@property (nonatomic, strong) BindData *bindData;
@property (nonatomic, strong) BaseRecord *serverRecord;
@property (nonatomic, unsafe_unretained) NSInteger status;
@property (nonatomic, copy) NSString *message;
@end//客户端回调接口
typedef void(^ServerCallbacka)(ServerData *);@interface ServerBindera : NSObject//单例
+(instancetype) getInstance;//注册接口
-(void) registWithAddr:(NSString *)addrentry:(NSString *)entryifaceName:(NSString *)ifaceNameifaceType:(NSString *)ifaceTypeclazz:(Class) clazz;//调用接口
-(void) callWithIfaceName:(NSString *)ifaceNamecb:(ServerCallback) cbparams:(NSDictionary *)params;//获取当前服务器时间
-(NSInteger) getServerTime;@end
//ServerBinder.m
#import "ServerBinder.h"@implementation BindData
@end@implementation ServerData
@end@implementation ServerBinder{NSInteger mTimeOffset;//服务器时间和本地时间的差值NSMutableDictionary *mBindDatas;//保存所有注册的数据,当然要保存了,不保存怎么调用?
}+(instancetype) getInstance{static ServerBinder *binder = nil;static dispatch_once_t dispatchOnce;dispatch_once(&dispatchOnce, ^{binder = [[ServerBinder alloc] init];});return binder;
}//注册某接口,只有注册过的接口才能使用 call 方法调用。全局每个接口只需调用一次
-(void) registWithAddr:(NSString *)addrentry:(NSString *)entryifaceName:(NSString *)ifaceNameifaceType:(NSString *)ifaceTypeclazz:(Class) clazz{BindData *data = [[BindData alloc] init];data.addr = addr;data.entry = entry;data.ifaceName = ifaceName;data.recordClass = clazz;data.ifaceType = ifaceType;[mBindDatas setObject:data forKey:entry];
}//调用某接口,在任何需要数据的时候调用。
-(void) callWithIfaceName:(NSString *)ifaceNamecb:(ServerCallback) cbparams:(NSDictionary *)params{if (![mBindDatas containsKey:ifaceName]) {NSLog(@"cant find this ifaceName: %@", ifaceName);return;}BindData *bindData = [mBindDatas objectForKey:ifaceName];if ([bindData.ifaceType isEqualToString:@"get"]) {[self getWithBindData:bindData andParams:params cb:cb];}else if ([bindData.ifaceType isEqualToString:@"post"]) {[self postWithBindData:bindData andParams:params cb:cb];}else if ([bindData.ifaceType isEqualToString:@"download"]) {[self downloadWithBindData:bindData andParams:params cb:cb];}else if ([bindData.ifaceType isEqualToString:@"upload"]) {[self uploadWithBindData:bindData andParams:params cb:cb];}
}//处理服务器返回数据
-(void) handleResponseWithBindData:(BindData *) bindData jsonDict:(NSDictionary *)jsonDict cb:(ServerCallback)cb{ServerData *serverData = [[ServerData alloc] init];serverData.bindData = bindData;serverData.status = [[jsonDict objectForKey:@"status"] intValue];serverData.message = [[jsonDict objectForKey:@"message"] stringValue];if (serverData.status == 1) {id data = [jsonDict objectForKey:@"data"];//把json数据转换成RecordserverData.serverRecord = [[[bindData.recordClass alloc] init]mj_setKeyValues:[data mj_JSONObject]];}if (cb) {cb(serverData);}//同步服务器时间if ([jsonDict containsKey:@"time"]) {NSInteger time = [[jsonDict objectForKey:@"time"] longValue];mTimeOffset = time - [self getLocalTime];}
}-(NSInteger) getLocalTime{//TODO 返回本地当前时间return 0;
}-(NSInteger) getServerTime{return [self getLocalTime] + mTimeOffset;
}-(void) getWithBindData:(BindData *)bindData andParams:(id)params cb:(ServerCallback)cb{//...TODO 使用AFNetWorking获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置[self handleResponseWithBindData:bindData jsonDict: jsonDict cb:cb];
}-(void) postWithBindData:(BindData *)bindData andParams:(id)params cb:(ServerCallback)cb{//...TODO 使用AFNetWorking获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置[self handleResponseWithBindData:bindData jsonDict: jsonDict cb:cb];
}-(void) downloadWithBindData:(BindData *)bindData andParams:(id)params cb:(ServerCallback)cb{//...TODO 使用AFNetWorking获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置[self handleResponseWithBindData:bindData jsonDict: jsonDict cb:cb];
}-(void) uploadWithBindData:(BindData *)bindData andParams:(id)params cb:(ServerCallback)cb{//...TODO 使用AFNetWorking获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置[self handleResponseWithBindData:bindData jsonDict: jsonDict cb:cb];
}@end
程序如何使用上述代码进行网络注册和调用呢?
android:
1. 需要自定义Application 假设定义为 MyApplication。
2. 在MyApplication中注册xUtils。
3. 新建某个接口对应的Record类: XXXRecord.java,这个类应该继承BaseRecord,具体写法参照。
4. 在MyApplication的onCreate方法中,添加代码:
ServerBinder.getInstance().regist("http://www.xxx.com", "api", "get_user_info", "get", XXXRecord.class);
5.在需要调用接口的地方这样写:
ServerBinder.getInstance().call("get_user_info", new ServerCallback(){@Overridepublic void onServerCallback(ServerData data){//data中包含很多数据,其中 data.serverRecord 就是我们的XXXRecord的实例了。XXXRecord *record = (XXXRecord)data.serverRecord;}
}, "uid", "1");
iOS:
1. 新建某个接口对应的Record类:XXXRecord,请参照MJExtension及其demo进行创建。
2. 在AppDelegate的如下方法中:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
添加代码
[[ServerBinder getInstance] registWithAddr: @"http://www.xxx.com" entry:@"api" ifaceName:@"get_user_info" ifaceType:@"get" Class:[XXXRecord class]];
3 . 在需要调用的地方这样写:
[ServerBinder getInstance] callWithIfaceName:@"get_user_info" cb:^(ServerData *serverData){//serverData中包含很多数据,其中 serverData.serverRecord 就是我们的XXXRecord的实例了。XXXRecord *record = (XXXRecord *)serverData.serverRecord;
} params:@{@"uid":1}];
至此,一个完整的网络模块就完成了。
仅需6步,教你轻易撕掉app开发框架的神秘面纱(4):网络模块的封装相关推荐
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(1):确定框架方案
遇到的问题 做游戏的时候用的是cocos2dx+lua,游戏开发自有它的一套框架机制.而现在公司主要项目要做android和iOS应用.本文主要介绍如何搭建简单易用的App框架. 如何解决 对 ...
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(5):数据持久化
遇到的问题 有的时候程序中需要全局皆可访问的变量,比如:用户是否登录,用户个人信息(用户名,地区,生日),或者一些其他信息如:是否是首次登录,是否需要显示新手引导等等. 其中有些数据需要持久化到本地硬 ...
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(3):构造具有个人特色的MVP模式
1. MVP的问题 之前我们说过MVP模式最大的问题在于:每写一个Activity/Fragment需要写4个对应的文件,对于一个简易的app框架来说太麻烦了.所以我们需要对MVP进行一定的简化. 关 ...
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(6):各种公共方法及工具类的封装
为什么要封装公共方法 封装公共方法有2方面的原因: 一是功能方面的原因:有些方法很多地方都会用,而且它输入输出明确,并且跟业务逻辑无关.比如检查用户是否登录,检查某串数字是否为合法的手机号.像这种方法 ...
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(2):MVP比MVC更好吗
对于程序框架的选择,由于android天然的MVC,本来不需要另外设计直接使用即可.但是我更加钟情于MVP模式,对于其将ui完全与业务逻辑分离的思路很赞同. 那么什么是业务逻辑?个人认为,对数据(即M ...
- 怎么快速修改gif尺寸?仅需三步教你改gif大小
很多时候我们从网上下载的gif动图或者是从电影.电视剧中截取的高清gif动图尺寸过大不方便传输,想要对gif图片尺寸修改的时候应该如何调整gif尺寸呢?很简单,使用[GIF中文网]的gif改大小(ht ...
- python爬虫excel数据_最简单的爬数据方法:Excel爬取数据,仅需6步
原标题:最简单的爬数据方法:Excel爬取数据,仅需6步 在看到这篇文章的时候,大家是不是都还停留在对python爬虫的迷恋中,今天就来教大家怎样使用微软的Excel爬取一个网页的后台数据,注:此方法 ...
- 隐藏esp_仅需一分钟教你看懂汽车内的隐藏功能,哪些功能是你不知道的?
车内的按键多种多样,而且越高档的车,按键就越多.除了少数国产车,绝大部分车辆的按键标识都是用英文字母表示,从而导致不少车主只能通过查看说明书才知道是什么意思. 今天小编整理了车内各种按键标识,不是很清 ...
- php 商城套餐搭配功能,速卖通商品搭配套餐功能已上线!设置速卖通搭配套餐仅需三步...
据雨果网获悉,速卖通商品搭配套餐功能已于 10 月 19 日上线.商品搭配套餐的主要功能及作用,主要是帮助速卖通的卖家,通过自行选择商品,设置不同商品间搭配优惠促销价格,提高商品推广内容的丰富性及专业 ...
最新文章
- 崔希凡javaWeb笔记day01~day03(2016年5月20日20:33:54)
- 为开源软件评级—商业就绪分级(BRR)模型
- Java 自增(++) 和 C语言中自增的区别
- RHEL6基础之三RHEL官网获取ISO镜像
- “约见”面试官系列之常见面试题第三十二篇之async和await(建议收藏)
- 输入文字自动生成图片_原来Word还可以自动生成图片和图表目录!
- php 图像居中裁剪函数,PHP 实现的自定义图像居中裁剪函数示例
- ccna视频教程 红头发讲解
- linux提示tree命令未安装,linux中没有tree命令,command not found,解决办法
- matlab fx函数图像,matlab 画两个自变量的函数图像
- 百度瓦片地图在线下载以及合成
- 计算思维是利用计算机,计算机思维论文
- 判定两个矩形框是否相交
- 2021年党员各种谈心谈话记录加空白表
- MindManager22直装版思维导图新功能
- Win10下用Strokeit的方法
- Python 股票分析入门
- 2023最新自动采集影视CMS程序源码+全开源的/功能也强大
- Android 疑难杂症
- PCIe扫盲——PCIe错误源详解(二)