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的子类
@Override
protected 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();
}
@Override
protected void onDestroy(){
super.onDestroy();
onExit();
if(mDataHandler != null){
mDataHandler.onExit();
}
}
//实现动态绑定DataHandler 这样每次声明BaseActivity的子类时,指定范型为真实的DataHandler子类后,本方法会自动初始化此DataHandler
private 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();
//点击事件的处理 begin
private HashMap<View, Object> mClickDataMap;//点击事件传入数据存储。
@Override
public 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(){
@Override
public 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.java
BaseActivity.java
BaseDataHandler.java
iOS:
IDataHandleInterface.h
IDataHandleInterface.m
BaseViewController.h
BaseViewController.m
BaseDataHandler.h
BaseDataHandler.m