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