僅需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中,也可以達到隔離的目的。

但是這種隔離依賴於我們對 ” 可能改變業務邏輯的操作 ” 的定義,而且這種操作的定義可能會隨着項目的進行而變化(增減)。

那麼可能會改變業務邏輯的操作有什麼?使用最多的無非就是如下幾種:

  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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章