1. MVP的问题

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

关于MVP模式是什么及其简单实现,可以参照:浅谈 MVP in Android

MVP模式最大的特点是:业务逻辑和页面元素的分离,以适应业务逻辑和页面各自可能发生的变化和多样性。

该模式在面向对象的语言角度对2者进行隔离,隔离的很彻底,但是代价也大。

2. 分析问题

为了解决这个问题,我们可以在另一个角度对两者进行不那么彻底的隔离:即从功能角度进行隔离。

我们之前说过,业务逻辑的数据来自如下几个方面:1.服务端返回数据 2.其它途径传入数据 3.需要传出的自定义数据

所以我们可以把在Activity(iOS:ViewContorller)中可能会改变业务逻辑的操作提取出来,放在DataHandler中,也可以达到隔离的目的。

但是这种隔离依赖于我们对 ” 可能改变业务逻辑的操作 ” 的定义,而且这种操作的定义可能会随着项目的进行而变化(增减)。

那么可能会改变业务逻辑的操作有什么?使用最多的无非就是如下几种:

  1. 网络请求
  2. 页面跳转
  3. 点击或其它类似事件

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模式相关推荐

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

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

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

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

  3. 仅需6步,教你轻易撕掉app开发框架的神秘面纱(4):网络模块的封装

    程序框架确定了,还需要封装网络模块. 一个丰富多彩的APP少不了网络资源的支持,毕竟用户数据要存储,用户之间也要交互,用户行为要统计等等. 使用开源框架 俗话说得好,轮子多了路好走,我们不需要自己造轮 ...

  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. 物联网背后的网络安全风险
  2. 谷歌CEO称公司预计每月收购一家小公司
  3. android 6.0谷歌,Android 6.0来了!谷歌月底要发布Android M系统
  4. 关于点名的简单python编程_如何用python编写一个简易的随机点名软件
  5. 图书推荐(持续更新)
  6. unity 使用mysql实现登录注册_用mysql实现登录注册功能
  7. pcl里面的RoPs特征(Rotational Projection Statistics)
  8. 11、进入保护模式-V
  9. 计算机关机电路,实用电脑电源关机全断电电路
  10. WinForm开发知识汇总
  11. JavaScript简单计算器
  12. H3C交换机 access端口配置
  13. matlab 梯度下降 求偏导,吴恩达机器学习课程课时12梯度下降算法中参数θ0,θ1求偏导...
  14. 柯桥增值税留抵如何退税?
  15. 多线程编程与资源同步API和示例
  16. 安卓手机怎样安装apk应用
  17. Ngrok 内网穿透神器
  18. 什么是撞库?如何预防撞库攻击
  19. UC研发团队——做自己的研发,让别人说去吧!(11月20日更新版)
  20. 产品上新需要注意什么 总结的思维导图分享给大家

热门文章

  1. Python高级函数--map/reduce
  2. JPush 使用教程
  3. idea上实现github代码同步
  4. win10红色警戒黑屏解决
  5. 关系管理系统:js代码生成select的出生日期
  6. 简评游戏人工智能相关的中文书(下)
  7. 如何在搜索结果出来之前,让页面显示“等待中。。。”
  8. linux mysql 命令 大全
  9. WEB 打印的相关技术分析
  10. ASP.NET虚拟主机的重大安全隐患