仅需6步,教你轻易撕掉app开发框架的神秘面纱(3):构造具有个人特色的MVP模式
1. MVP的问题
之前我们说过MVP模式最大的问题在于:每写一个Activity/Fragment需要写4个对应的文件,对于一个简易的app框架来说太麻烦了。所以我们需要对MVP进行一定的简化。
关于MVP模式是什么及其简单实现,可以参照:浅谈 MVP in Android
MVP模式最大的特点是:业务逻辑和页面元素的分离,以适应业务逻辑和页面各自可能发生的变化和多样性。
该模式在面向对象的语言角度对2者进行隔离,隔离的很彻底,但是代价也大。
2. 分析问题
为了解决这个问题,我们可以在另一个角度对两者进行不那么彻底的隔离:即从功能角度进行隔离。
我们之前说过,业务逻辑的数据来自如下几个方面:1.服务端返回数据 2.其它途径传入数据 3.需要传出的自定义数据
所以我们可以把在Activity(iOS:ViewContorller)中可能会改变业务逻辑的操作提取出来,放在DataHandler中,也可以达到隔离的目的。
但是这种隔离依赖于我们对 ” 可能改变业务逻辑的操作 ” 的定义,而且这种操作的定义可能会随着项目的进行而变化(增减)。
那么可能会改变业务逻辑的操作有什么?使用最多的无非就是如下几种:
- 网络请求
- 页面跳转
- 点击或其它类似事件
3. 解决方案
具体如何实现呢?
首先我们定义一个接口(IDataHandlerInterface),包含上述3种事件的函数。然后令Activity(iOS:UIViewController,下同)和DataHandler实现该接口。
当对应事件发生时,依次调用DataHandler和Activity中的对应函数。
对此,我们需要定义BaseActivity(iOS:BaseViewController)类,把上述操作封装在此类中,后续自定义的Activity(iOS:ViewController)都需要继承BaseActivity(iOS:BaseViewController)。
//android: IDataHandlerInterface.java
public interface class IDataHandlerInterface{//网络请求,关于网络请求细节后续会介绍,这里ServerData就是服务端返回数据//如有人调用BaseDataHandler中的callserver,此函数会在接口回调后自动调用public void onServerCallback(ServerData data);//页面跳转public void onEnter();public void onExit();//点击事件public boolean onClick(View v, Object data);
}
//iOS: IDataHandlerInterface.h
@protocol IDataHandlerInterface <NSObject>
//网络请求,关于网络请求细节后续会介绍,这里ServerData就是服务端返回数据
//如有人调用BaseDataHandler中的callserver,此函数会在接口回调后自动调用
-(void) onServerCallbackWithData:(ServerData*) data;
//页面跳转
-(void) onEnter;
-(void) onExit;
//点击事件
-(BOOL) onClickWithView:(UIView *)view andData:(id)data;
@end
4. 其它问题
除了继承IDataHandleInterface之外,BaseActivity(iOS:BaseViewController)还有其它的责任,它需要对生命周期进行封装重构,使不同的函数职责分明,可以在增加可读性的同时,令不同的程序员更容易写出一致的代码。
如何重构BaseActivity(iOS:BaseViewController)生命周期呢?
其实很简单,BaseActivity(iOS:BaseViewController)的职责是显示UI控件,而习惯上UI相关的代码,大多数都是写在OnCreate(iOS:viewDidLoad)中。这样写有些违犯设计模式中的单一原则,因为会使一些UI无关的 私有变量 和 添加事件监听 的操作都放在OnCreate中,所以这些操作应该分离出来。
而BaseActivity(iOS:BaseViewController)也需要同BaseDataHandler的实例相互引用。这一点耦合是难以避免的。
另外,如何连接BaseActivity(iOS:BaseViewController)和BaseDataHandler是一个不小的问题。
有什么问题呢?是这样的,因为BaseActivity(iOS:BaseViewController)中引用的是BaseDataHandler的实例。
所以当子类继承BaseActivity(iOS:BaseViewController)后,只能拿到BaseDataHandler的引用。而不能拿到真实的DataHandler引用,这样每次想要调用DataHandler子类中某些不在BaseDataHandler中的方法时就需要强转。
这是一个很不友好的,带有写重复代码嫌疑的操作。
为了解决这个问题,我们需要使用范型,对DataHandler和Activity(iOS: ViewController)进行编译时自动绑定。
在iOS 中没有泛型的概念,但我们仍然可以使用 @property覆盖 及 @dynamic注解 来解决此问题。
android版代码及注释如下:
//BaseActivity.java
public abstract class BaseActivity<D extends BaseDataHandler> extends FragmentActivity implements IDataHandleInterface, View.OnClickListener{private final static String TAG = "BaseActivity";private D mDataHandler;//业务逻辑处理,使用泛型令子类可以动态绑定BaseDataHandler的子类@Overrideprotected void onCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);createDataHandler();mClickDataMap = new HashMap<>();if(mDataHandler != null){mDataHandler.setActivity(this);mDataHandler.onEnter();}onEnter();initArgs();initViews();initEvents();mDataHandler.loadDatas();}@Overrideprotected void onDestroy(){super.onDestroy();onExit();if(mDataHandler != null){mDataHandler.onExit();}}//实现动态绑定DataHandler 这样每次声明BaseActivity的子类时,指定范型为真实的DataHandler子类后,本方法会自动初始化此DataHandlerprivate void createDataHandler(){Class genericClass = (Class)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];try{mDataHandler = (D)genericClass.newInstance();}catch(Exception e){Log.e(TAG, e.toString());}}//子类获取DataHandler实例就是真实的类对象引用protected D getDataHandler(){return mDataHandler} //内部变量初始化protected abstract initArgs();//ui组件初始化protected abstract initViews();//添加点击事件protected abstract initEvents();//点击事件的处理 beginprivate HashMap<View, Object> mClickDataMap;//点击事件传入数据存储。@Overridepublic void onClick(View v){Object data = mClickDataMap.get(v);if(onClick(v, data)){//因为有些点击会因为某些错误,如输入不合法,不需要修改数据。所以在此判断if(mDataHandler != null){mDataHandler.onClick(v, data);}}}//子类需要调用此方法对View进行点击事件绑定。当然真实情况下可能不局限于点击事件,有可能还会有滑动/长按等等类似事件,这种情况下就需要扩展此方法。protected void addOnClickListener(View v, Object data){v.setOnClickListener(this);mClickDataMap.put(v, data);}//点击事件的处理 end//页面跳转 begin//这只是个演示版本,没有考虑有返回值的情况,具体细节需要进行再次开发。protected void jumpToPage(Class c){Intent i = new Intent(this, c);if(mDataHandler != null){mDataHandler.pushDataForJumpPage(i);}startActivity(i);}//页面跳转 end
}
//BaseDataHandler.java
public abstract BaseDataHandler<A extends BaseActivity> implements IDataHandleInterface{private final static String TAG = "BaseDataHandler";private A mActivity;//页面引用原理同上/*package*/void setActivity(A activity){mActivity = activity;}public A getActivity(){return mActivity}protected void callServer(String method, String ...params){//TODO:进行网络请求,此处留空,后续完成网络模块后,填充此方法// 伪代码如下:Server.call(method, params, new ServerCallback(){@Overridepublic void onResponse(ServerData data){if(data.status == succ){onServerCallback(data);if(mActivity != null){mActivity.onServerCallback(data);}}else{tip("接口调用失败,错误码:" + data.code + ", 错误信息:" + data.message);}}});}//有些页面刚进入时需要调用接口,这种情况的调用需写在此方法中。//第一次接口调用不能写在onEnter中是因为:onEnter时页面元素还没有构造,而回调用可能会对UI组件进行操作,所以可能会引起null异常。//所以增加loadDatas方法,此方法调用时,页面元素已经构建完毕。//OnEnter方法在构建页面之前调用的原因是,onEnter可能会接收来自其它页面的数据,为了令此数据全局有效,所以尽早调用是比较妥当的。//因此请在onEnter方法中获取来自其它页面传入的Intent内存储的数据。protected abstract void loadDatas();//页面跳转,因为页面跳转时,可能需要传递一些数据,而这些数据自然就在DataHandler中//通过此方法,向Intent中传递数据,子类可以根据intent中的class判断不同的页面protected abstract void pushDataForJumpPage(Intent intent);
}
iOS版代码及注释如下:
// BaseViewController.h
#import <UIKit/UIKit.h>
#import "BaseDataHandler.h"
#import "IDataHandlerInterface.h"@class BaseDataHandler;
@interface BaseViewController : UIViewController<IDataHandlerInterface>
@property (nonatomic, strong) BaseDataHandler *dataHandler;//子类需使用此方法添加点击事件
-(void) addOnClickListenerWithView:(UIView *)v andData:(id)data;//子类需使用此方法进行页面跳转
-(void) jumpToPageWithClass:(Class) clazz andDataHandlerClazz:(Class) dhClazz andData:(NSDictionary *)data isNavCtl:(BOOL) isNavCtl;//子类需使用此方法关闭页面
-(void) closePageWithIsNavCtl:(BOOL) isNavCtl;//!!!如下方法需要子类重载-(void) initArgs;//内部变量初始化-(void) initViews;//ui组件初始化-(void) initEvents;//添加点击事件-(void)onServerCallbackWithData:(id)data;-(void)onEnter;-(void)onExit;-(BOOL)onClickWithView:(UIView *)view andData:(id)data;
@end
//BaseViewController.m
#import "BaseViewController.h"
@implementation BaseViewController{//点击事件传入数据存储。data和view同步存储所以index相同的object为一对。NSMutableArray *mClickViewMap;NSMutableArray *mClickDataMap;
}- (instancetype)initWithDataHandler:(Class) dataHandlerClazz
{self = [super init];if (self) {self.dataHandler = [dataHandlerClazz new];mClickViewMap = [NSMutableArray new];mClickDataMap = [NSMutableArray new];if (self.dataHandler) {self.dataHandler.viewController = self;[self.dataHandler onEnter];}[self onEnter];}return self;
}-(void)viewDidLoad{[super viewDidLoad];[self initArgs];[self initViews];[self initEvents];[self.dataHandler loadDatas];
}-(void) addOnClickListenerWithView:(UIView *)v andData:(id)data{if ([v isKindOfClass:[UIButton class]]) {UIButton *btn = (UIButton *)v;[btn removeTarget:self action:@selector(onClickInner:) forControlEvents:UIControlEventTouchUpInside];[btn addTarget:self action:@selector(onClickInner:) forControlEvents:UIControlEventTouchUpInside];} else {UITapGestureRecognizer *tapGes = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onClickInner:)];for (NSInteger i = v.gestureRecognizers.count - 1; i >= 0; i--) {UIGestureRecognizer *gesture = v.gestureRecognizers[i];if ([gesture isKindOfClass:[UITapGestureRecognizer class]]) {[v removeGestureRecognizer:gesture];}}[v addGestureRecognizer:tapGes];}if (data) {[mClickViewMap addObject:v];[mClickDataMap addObject:data];}
}-(void) onClickInner:(UIView *)view{id data = nil;if ([mClickViewMap containsObject:view]) {data = [mClickDataMap objectAtIndex:[mClickViewMap indexOfObject:view]];}//因为有些点击会因为某些错误,如输入不合法,不需要修改数据。所以在此判断if([self onClickWithView:view andData:data]){if (self.dataHandler) {[self.dataHandler onClickWithView:view andData:data];}}
}-(void) jumpToPageWithClass:(Class) clazz andDataHandlerClazz:(Class) dhClazz andData:(NSDictionary *)data isNavCtl:(BOOL) isNavCtl{if (![clazz isSubclassOfClass:[BaseViewController class]] ||![dhClazz isSubclassOfClass:[BaseDataHandler class]]) {NSLog(@"错误:clazz必须是BaseViewController的子类,dhClazz必须是BaseDataHandler的子类");return;}//数据NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithDictionary:data];if (self.dataHandler) {[self.dataHandler pushDataForJumpPageWithDict:dict];}//跳转BaseViewController *ctl = [[clazz alloc] initWithDataHandler:dhClazz];ctl.dataHandler.inputData = dict;if (self.navigationController && isNavCtl) {[self.navigationController pushViewController:[[UINavigationController alloc] initWithRootViewController:ctl] animated:YES];}else{//防止跳转时有延迟或跳转失败。dispatch_async(dispatch_get_main_queue(), ^{[self presentViewController:ctl animated:YES completion:nil];});}
}-(void) closePageWithIsNavCtl:(BOOL) isNavCtl{if (self.navigationController && isNavCtl) {[self.navigationController popViewControllerAnimated:YES];}else{dispatch_async(dispatch_get_main_queue(), ^{[self dismissViewControllerAnimated:YES completion:nil];});}//退出逻辑[self onExit];if (self.dataHandler) {[self.dataHandler onExit];}
}//-----------------------------
-(void) initArgs{}-(void) initViews{}-(void) initEvents{}-(void) onServerCallbackWithData:(id)data{}-(void) onEnter{}-(void) onExit{}-(BOOL) onClickWithView:(UIView *)view andData:(id)data{return NO;}
@end
//BaseDataHandler.h
#import <Foundation/Foundation.h>
#import "BaseViewController1.h"
#import "IDataHandlerInterface.h"@class BaseViewController;
@interface BaseDataHandler : NSObject<IDataHandlerInterface>
@property (nonatomic, weak) BaseViewController *viewController;
@property (nonatomic, strong) NSMutableDictionary *inputData;-(void) callServerWithMethod:(NSString *)method andParams:(id)params;//!!!下列方法子类需覆盖//loadDatas: 有些页面刚进入时需要调用接口,这种情况的调用需写在此方法中。
//第一次接口调用不能写在onEnter中是因为:onEnter时页面元素还没有构造,而回调用可能会对UI组件进行操作,所以可能会引起null异常。
//所以增加loadDatas方法,此方法调用时,页面元素已经构建完毕。
//OnEnter方法在构建页面之前调用的原因是,onEnter可能会接收来自其它页面的数据,为了令此数据全局有效,所以尽早调用是比较妥当的。
//因此请在onEnter方法中获取来自其它页面传入的Intent内存储的数据。
-(void) loadDatas;//页面跳转,因为页面跳转时,可能需要传递一些数据,而这些数据自然就在DataHandler中
//通过此方法,向Intent中传递数据,子类可以根据intent中的class判断不同的页面
-(void) pushDataForJumpPageWithDict:(NSMutableDictionary *)dict;-(void)onServerCallbackWithData:(id)data;-(void)onEnter;-(void)onExit;-(BOOL)onClickWithView:(UIView *)view andData:(id)data;@end
//BaseDataHandler.m
#import "BaseDataHandler.h"@implementation BaseDataHandler-(void) callServerWithMethod:(NSString *)method andParams:(id)params{//TODO:进行网络请求,此处留空,后续完成网络模块后,填充此方法// 伪代码如下:[Server callWithMethod:method andParams:params andCb:^(ServerData data){if(data.status == succ){[self onServerCallbackInnerWithData:data]}else{tip("接口调用失败,错误吗:" + data.code + ", 错误信息:" + data.message);}}];
}-(void) loadDatas{}-(void) pushDataForJumpPageWithDict:(NSMutableDictionary *)dict{}-(void) onServerCallbackWithData:(id)data{}-(void) onEnter{}-(void) onExit{}-(BOOL) onClickWithView:(UIView *)view andData:(id)data{return NO;}@end
android的使用方法不用多说,建立2个文件分别继承BaseActivity和BaseDataHandler,然后泛型指定为对方即可。后续使用同正常使用Activity。
iOS则需要额外做一点事情,进行实际的DataHandler与实际的ViewController对象之间的绑定(也就是android中泛型起到的作用)。并且跳转页面必须使用BaseViewController中的 jumpToPageWithClass方法。
例子如下所示:
//MyViewController.h
#import "BaseViewController.h"
#import "MyDataHandler.h"@class MyDataHandler; //!!!!!@@@@@[1]
@interface MyViewController : BaseViewController
@property (nonatomic, strong) MyDataHandler *dataHandler; //!!!!!@@@@@[2]
@end
//MyViewController.m
#import "MyViewController.h"
@implementation MyViewController{NSDictionary *mData;UILabel *mLabel;
}@dynamic dataHandler;//!!!!!@@@@@[3]//!!!如下方法需要子类重载-(void) initArgs{//内部变量初始化mData = [NSDictionary new];
}-(void) initViews{//ui组件初始化UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 30)];label.text = @"你好";label.font = [UIFont systemFontOfSize:15];label.textColor = [UIColor redColor];[self.view addSubview:label];mLabel = label;UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];btn.titleLabel.text = @"---按钮---";[self.view addSubview:btn];[self addOnClickListenerWithView:btn andData:@"btn"];
}-(void) initEvents{//添加点击事件
}-(void) onClickBtn{//点击变色mLabel.textColor = [mLabel.textColor isEqual:[UIColor redColor]] ? [UIColor blueColor]: [UIColor redColor];
}-(void)onServerCallbackWithData:(id)data{NSLog(@"onServerCallbackWithData");
}-(void)onEnter{NSLog(@"onEnter");
}-(void)onExit{NSLog(@"onExit");
}-(BOOL)onClickWithView:(UIView *)view andData:(id)data{if ([data isEqualToString:@"btn"]) {[self onClickBtn];}return YES;
}@end
//MyDataHandler.h
#import "BaseDataHandler.h"
#import "MyViewController.h"@class MyViewController;//!!!!!@@@@@[4]
@interface MyDataHandler : BaseDataHandler
@property (nonatomic, weak) MyViewController *viewController;//!!!!!@@@@@[5]
@end
//MyDataHandler.m
#import "MyDataHandler.h"@implementation MyDataHandler
@dynamic viewController;//!!!!!@@@@@[6]@end
[注意:]标记为 “//!!!!!@@@@@[x]“ 的地方就是动态绑定ViewController 和 DataHandler实例所写的代码,项目中可以把他们封装到宏定义中,更加方便使用。
另外,上述代码只是一个可用框架的最小集合,如果使用在项目中可根据需要进行扩展。比如:需要对Activity的生命周期进行关注;需要关注页面隐藏和显示的事件等等。
到此为止,我们已经搭建好了一个具有个人风格的MVP模式了。
下面是代码清单:
android:IDataHandleInterface.javaBaseActivity.javaBaseDataHandler.java
iOS:IDataHandleInterface.hIDataHandleInterface.mBaseViewController.hBaseViewController.mBaseDataHandler.hBaseDataHandler.m
仅需6步,教你轻易撕掉app开发框架的神秘面纱(3):构造具有个人特色的MVP模式相关推荐
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(1):确定框架方案
遇到的问题 做游戏的时候用的是cocos2dx+lua,游戏开发自有它的一套框架机制.而现在公司主要项目要做android和iOS应用.本文主要介绍如何搭建简单易用的App框架. 如何解决 对 ...
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(5):数据持久化
遇到的问题 有的时候程序中需要全局皆可访问的变量,比如:用户是否登录,用户个人信息(用户名,地区,生日),或者一些其他信息如:是否是首次登录,是否需要显示新手引导等等. 其中有些数据需要持久化到本地硬 ...
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(4):网络模块的封装
程序框架确定了,还需要封装网络模块. 一个丰富多彩的APP少不了网络资源的支持,毕竟用户数据要存储,用户之间也要交互,用户行为要统计等等. 使用开源框架 俗话说得好,轮子多了路好走,我们不需要自己造轮 ...
- 仅需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 日上线.商品搭配套餐的主要功能及作用,主要是帮助速卖通的卖家,通过自行选择商品,设置不同商品间搭配优惠促销价格,提高商品推广内容的丰富性及专业 ...
最新文章
- 物联网背后的网络安全风险
- 谷歌CEO称公司预计每月收购一家小公司
- android 6.0谷歌,Android 6.0来了!谷歌月底要发布Android M系统
- 关于点名的简单python编程_如何用python编写一个简易的随机点名软件
- 图书推荐(持续更新)
- unity 使用mysql实现登录注册_用mysql实现登录注册功能
- pcl里面的RoPs特征(Rotational Projection Statistics)
- 11、进入保护模式-V
- 计算机关机电路,实用电脑电源关机全断电电路
- WinForm开发知识汇总
- JavaScript简单计算器
- H3C交换机 access端口配置
- matlab 梯度下降 求偏导,吴恩达机器学习课程课时12梯度下降算法中参数θ0,θ1求偏导...
- 柯桥增值税留抵如何退税?
- 多线程编程与资源同步API和示例
- 安卓手机怎样安装apk应用
- Ngrok 内网穿透神器
- 什么是撞库?如何预防撞库攻击
- UC研发团队——做自己的研发,让别人说去吧!(11月20日更新版)
- 产品上新需要注意什么 总结的思维导图分享给大家