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

9 篇文章 0 订阅
6 篇文章 0 订阅

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的子类
    @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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值