程序框架确定了,还需要封装网络模块。
一个丰富多彩的APP少不了网络资源的支持,毕竟用户数据要存储,用户之间也要交互,用户行为要统计等等。
俗话说得好,轮子多了路好走,我们不需要自己造轮子,拿来主义就行了。
android网络模块核心功能使用xUtils3开源框架来完成。
而iOS则使用AFNetWorking,别告诉我你没听说过AFNetworking。
xUtils3拥有4大功能:数据库,视图注解,网络,图片(支持webp)。
AFNetWorking则包含网络和图片2部分。
我们只需要用到其中的网络模块和图片缓存模块。
《App研发录》中强烈要求把后台返回的json数据转换成类实例Record(有些人喜欢称为model,即:MVC中的M,而个人习惯称之为Record,而Model的使用我更倾向于可共享可本地化的全局单例)类。在业务逻辑中使用的是这些类实例化后的对象。
这样做的好处有3个:
为了达到上述目的,我们需要再次引入一个第三方库,来自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是一个单例,它需要用户输入后台接口的名字后,然后输出一个对应的存储了所有返回的服务端数据的Record。
ServerBinder中需要这样一个方法:regist,表示注册某个接口,只有在ServerBinder中注册过的服务端接口,留下了必要信息,后续才能够调用。
我们需要分析一下服务端调用地址的构成,来决定此方法的传入参数:
服务端接口往往是这样的,http://xxx.com/api/user_info?id=1000
其中可变的部分为:
这样,我们的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){
//初始化BindData
BindData 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 *)addr
entry:(NSString *)entry
ifaceName:(NSString *)ifaceName
ifaceType:(NSString *)ifaceType
clazz:(Class) clazz;
//调用接口
-(void) callWithIfaceName:(NSString *)ifaceName
cb:(ServerCallback) cb
params:(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 *)addr
entry:(NSString *)entry
ifaceName:(NSString *)ifaceName
ifaceType:(NSString *)ifaceType
clazz:(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 *)ifaceName
cb:(ServerCallback) cb
params:(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数据转换成Record
serverData.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(){
@Override
public 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}];
至此,一个完整的网络模块就完成了。