程序框架确定了,还需要封装网络模块。

一个丰富多彩的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):网络模块的封装相关推荐

  1. 仅需6步,教你轻易撕掉app开发框架的神秘面纱(1):确定框架方案

    遇到的问题   做游戏的时候用的是cocos2dx+lua,游戏开发自有它的一套框架机制.而现在公司主要项目要做android和iOS应用.本文主要介绍如何搭建简单易用的App框架. 如何解决   对 ...

  2. 仅需6步,教你轻易撕掉app开发框架的神秘面纱(5):数据持久化

    遇到的问题 有的时候程序中需要全局皆可访问的变量,比如:用户是否登录,用户个人信息(用户名,地区,生日),或者一些其他信息如:是否是首次登录,是否需要显示新手引导等等. 其中有些数据需要持久化到本地硬 ...

  3. 仅需6步,教你轻易撕掉app开发框架的神秘面纱(3):构造具有个人特色的MVP模式

    1. MVP的问题 之前我们说过MVP模式最大的问题在于:每写一个Activity/Fragment需要写4个对应的文件,对于一个简易的app框架来说太麻烦了.所以我们需要对MVP进行一定的简化. 关 ...

  4. 仅需6步,教你轻易撕掉app开发框架的神秘面纱(6):各种公共方法及工具类的封装

    为什么要封装公共方法 封装公共方法有2方面的原因: 一是功能方面的原因:有些方法很多地方都会用,而且它输入输出明确,并且跟业务逻辑无关.比如检查用户是否登录,检查某串数字是否为合法的手机号.像这种方法 ...

  5. 仅需6步,教你轻易撕掉app开发框架的神秘面纱(2):MVP比MVC更好吗

    对于程序框架的选择,由于android天然的MVC,本来不需要另外设计直接使用即可.但是我更加钟情于MVP模式,对于其将ui完全与业务逻辑分离的思路很赞同. 那么什么是业务逻辑?个人认为,对数据(即M ...

  6. 怎么快速修改gif尺寸?仅需三步教你改gif大小

    很多时候我们从网上下载的gif动图或者是从电影.电视剧中截取的高清gif动图尺寸过大不方便传输,想要对gif图片尺寸修改的时候应该如何调整gif尺寸呢?很简单,使用[GIF中文网]的gif改大小(ht ...

  7. python爬虫excel数据_最简单的爬数据方法:Excel爬取数据,仅需6步

    原标题:最简单的爬数据方法:Excel爬取数据,仅需6步 在看到这篇文章的时候,大家是不是都还停留在对python爬虫的迷恋中,今天就来教大家怎样使用微软的Excel爬取一个网页的后台数据,注:此方法 ...

  8. 隐藏esp_仅需一分钟教你看懂汽车内的隐藏功能,哪些功能是你不知道的?

    车内的按键多种多样,而且越高档的车,按键就越多.除了少数国产车,绝大部分车辆的按键标识都是用英文字母表示,从而导致不少车主只能通过查看说明书才知道是什么意思. 今天小编整理了车内各种按键标识,不是很清 ...

  9. php 商城套餐搭配功能,速卖通商品搭配套餐功能已上线!设置速卖通搭配套餐仅需三步...

    据雨果网获悉,速卖通商品搭配套餐功能已于 10 月 19 日上线.商品搭配套餐的主要功能及作用,主要是帮助速卖通的卖家,通过自行选择商品,设置不同商品间搭配优惠促销价格,提高商品推广内容的丰富性及专业 ...

最新文章

  1. 崔希凡javaWeb笔记day01~day03(2016年5月20日20:33:54)
  2. 为开源软件评级—商业就绪分级(BRR)模型
  3. Java 自增(++) 和 C语言中自增的区别
  4. RHEL6基础之三RHEL官网获取ISO镜像
  5. “约见”面试官系列之常见面试题第三十二篇之async和await(建议收藏)
  6. 输入文字自动生成图片_原来Word还可以自动生成图片和图表目录!
  7. php 图像居中裁剪函数,PHP 实现的自定义图像居中裁剪函数示例
  8. ccna视频教程 红头发讲解
  9. linux提示tree命令未安装,linux中没有tree命令,command not found,解决办法
  10. matlab fx函数图像,matlab 画两个自变量的函数图像
  11. 百度瓦片地图在线下载以及合成
  12. 计算思维是利用计算机,计算机思维论文
  13. 判定两个矩形框是否相交
  14. 2021年党员各种谈心谈话记录加空白表
  15. MindManager22直装版思维导图新功能
  16. Win10下用Strokeit的方法
  17. Python 股票分析入门
  18. 2023最新自动采集影视CMS程序源码+全开源的/功能也强大
  19. Android 疑难杂症
  20. PCIe扫盲——PCIe错误源详解(二)

热门文章

  1. C# http 性能优化500毫秒到 60 毫秒
  2. SpringBoot实战(十四)之整合KafKa
  3. 003小插曲之变量和字符串
  4. FilenameFilter的使用
  5. Linux如何查看当前目录下文件的个数
  6. 买了《精通spring 2.0》
  7. MIS开发中.net Framework的打印功能
  8. 利用.NET的XML序列化解决系统配置问题
  9. XML的简单读取与写入
  10. 使用 Global.asax 文件