MVP框架的演化

MVP這種架構在Android界已經基本成為標配,MVP本身也有很多寫法和變種,當然,沒有最好的架構,只有最合適的架構,具體架構要怎么寫,還是要看實際項目的需要。
我們在這里簡單梳理一下MVP的一些演化版本,希望為具體的項目實現提供一點參考。
MVP本身的概念,就是把Model、View和Presenter相互解耦,大概可以這樣理解:


MVP的基本概念

各自分工如下:

  • View負責外部界面交互,不直接處理業務邏輯
  • Presenter負責內部業務邏輯,不直接處理業務數據
  • Model負責核心業務數據,與數據庫和網絡進行數據交互

原始MVP

如果僅從分工的角度實現MVP,只需要發生兩次引用:

  • 向View里引用Presenter,(因為View都是Fragment或Activity,有特定的構造函數,所以一般采用set方式引用),以處理具體的內部業務邏輯,代碼形如:
public class TasksFragment extends Fragment{
  ...
  private Presenter mPresenter;
  public void setPresenter(Presenter presenter) {
        mPresenter = presenter;
    }
  ...
}
  • 向Presenter里引用Model和View,其中View需要通過接口封裝一下再引用(一般在構造函數中引用),引用Model為業務邏輯提供核心的業務數據,引用View操作與界面相關的業務邏輯,代碼形如:
public class Presenter{
  ...
  private Repository mRepository;//model的實現這里不再展開
  private TasksFragment mView;
  public Presenter(Repository tasksRepository, TasksFragment tasksView) {//presenter里引用model
        mRepository = tasksRepository;
        mTView = tasksView;
        mTasksView.setPresenter(this);//view里引用presenter
    }
  ...
}

這就是一個最原始的MVP的實現,這個版本有一個嚴重的問題,就是可維護性太差!
這版MVP雖然實現了各司其職,但其實質只不過是把代碼拆到了不同的文件里,在實現中,M、V和P都引用了實體類的實例,形成了非常緊密的耦合,它其實只是實現了這樣的效果:


實際效果

很顯然,難以復用,難以擴展,未來的維護簡直是個災難。
為了解耦合,很自然地,要使用接口去解耦合。

演化1-Google Architecture

Google在github上開源的architecture是個教科書般的MVP框架,它是這樣做的:

  • V和P的接口化和注入
    View和Presenter不再引用實體類,而是引用抽象接口,View里引用的Presenter的接口,Presenter里引用的也是View的接口,這樣的View和Presenter的代碼形如:
public class TasksFragment extends Fragment implements IView{//實現接口以便注入到Presenter
  ...
  private IPresenter mPresenter;
  public void setPresenter(IPresenter presenter) {//view已有特定的構造函數,以set方式注入為宜
        mPresenter = presenter;
    }
  ...
}

public class Presenter implements IPresenter{//實現接口以便注入到view
  ...
  private Repository mRepository;//model的實現這里不再展開
  private IView mView;
  public Presenter(Repository tasksRepository, IView tasksView) {//presenter里注入model和view
        mRepository = tasksRepository;
        mTView = tasksView;
        mTasksView.setPresenter(this);//view里注入presenter
    }
  ...
}

如果愿意的話,model也可以采用接口注入的形式(google architecture并沒有做model的接口注入,是為了確保引用的實例是一個全局唯一的數據層單例,這樣容易避免污染數據),這樣就能實現一個完好解耦的MVP:


可維護性良好MVP
  • 集中管理V和P的接口
    其實就是把V和P的接口放在同一個接口文件下了,代碼形如:
public interface ITasksContract {
    interface IView{...}
    interface IPresenter{...}
}

這樣做有兩個好處:
1.從一組業務來講,業務邏輯和界面邏輯在同一個文件中定義,極具連貫性,極大地方便了閱讀、理解和維護(這也會引導你先從接口開始寫代碼)
2.從多組業務來講(App一般有多組業務),便于管理好多個V和P的接口,這些接口天然按照業務分別寫在不同文件里,擴展和引用更加清晰,不易出錯

google architecture還做了一項改進,為V和P的接口定義了更基礎的接口,在基礎接口中統一定義了View注入Presenter的行為和Presenter開啟業務工作的行為,代碼形如:

public interface BaseView<T> {//用泛型定義Presenter
    void setPresenter(T presenter);//用set注入Presenter
}

public interface BasePresenter {
    void start();//開啟業務工作
}

你自己實現的V和P的接口,只要繼承基礎接口,就能保證MVP基礎行為的一致性,這樣你的V和P就可以更加專注于業務
(除了教科書般的MVP,google architecture還提供了教科書般的數據Model層實現,不過這里就不做展開了)

演化2-泄露的問題

上面的這種做法,有一個潛在的問題,就是內存泄露
我們知道,Presenter為了實現業務邏輯,一手持有數據Model,一手持有View,這里面有一個隱含的bug:
數據Model在處理數據時,無論是處理本地數據還是網絡數據,都是耗時操作,是不能在主線程運行的;而View,是必須在主線程運行的。這就容易產生一個問題,當View關閉退出時,Presenter可能還在異步線程里工作,而且Presenter還持有著View的實例——標準的內存泄露場景
要避免持有型的內存泄露,一個很有效的辦法就是把強引用的持有變成弱引用,就是說,在Presenter里,要用WeakReference的方式去持有View,實現代碼形如:

    protected WeakReference<T> mViewRef; // view 的弱引用
    public void attachView(T view){//持有View
        mViewRef = new WeakReference<T>(view);
    }
    public void detachView(){
        if (mViewRef != null){
            mViewRef.clear();
            mViewRef = null;
        }
    }
    public T getView() {//獲取view的實例
        return mViewRef.get();
    }

這段代碼其實是通用代碼,根據聚焦業務的原則,應該抽象為基礎行為,而接口是不能實現任何方法的,所以,這段代碼只能通過抽象類實現通用化,整個類的代碼形如:

public abstract class MVPBasePresenter<T> {
    protected WeakReference<T> mViewRef; // view 的弱引用
    public void attachView(T view){//持有View
        mViewRef = new WeakReference<T>(view);
    }
    public void detachView(){
        if (mViewRef != null){
            mViewRef.clear();
            mViewRef = null;
        }
    }
    public T getView() {//獲取view的實例
        return mViewRef.get();
    }
}

其中,attachView和detachView要在View的相應的生命周期中調用,這樣的話,我們又需要為View實現相關的抽象類,Fragment和Activity都需要

//需要兩個泛型類型,一個用來繼承Presenter的抽象類,而這個Presenter抽象類又需要一個View的泛型
public abstract class MVPBaseFragment<V,T extends MVPBasePresenter<V>> extends Fragment {
    protected T mPresenter;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPresenter = createPresenter();
        mPresenter.attachView((V)this);
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
        if(mPresenter!=null)
        mPresenter.detachView();
    }
    protected abstract T createPresenter();
}

這樣一個業務Fragment在實例化時,代碼形如:

//ICategoryContract.View是IView接口
//我們還定義了ICategoryContract.Presenter作為IPresenter接口
//CategoryPresenter繼承了MVPBasePresenter抽象了和IPresenter接口
public class MainFragment 
extends MVPBaseFragment<ICategoryContract.View,CategoryPresenter> 
implements ICategoryContract.View {...}

我們實際上把Presenter抽象類和IPresenter業務接口做了分離,把View抽象類和IView業務接口做了分離,基礎行為和業務邏輯互不干擾。
Activity的代碼內容類似,這里不再重復。
到了實際項目中,V和P分別繼承對應的抽象類,因為抽象類里已經實現了弱引用和相關的管理,所以我們可以專注于業務邏輯的實現。
不過,這樣做帶來兩個問題:

  • 如果在View的構造函數中自動處理Presenter的實例化,實際上會束縛了我們自己的寫作方式,比如我們的Presenter需要注入Model,就不能用構造方式注入;更嚴重的是,如果我們在Presenter初始化時需要設置某些UI控件,因為抽象類的oncreate需要先于業務類的oncreate去執行(業務類里需要先執行super.oncreate),會遇到UI控件不能及時初始化的問題。
  • Android的View其實是在不斷擴張的,以Activity為例,常見的就包括Activity、AppCompatActivity、FragmentActivity、RxAppCompatActivity等,如果使用這種抽象類的模式,每遇到一種Activity,就得去做一個對應的抽象類,可擴展性很差。

參照Google的做法,我們應該再多做一點接口的文章

演化3-View的剝離

我們回頭再看一遍Presenter抽象類

//Presenter抽象類
public abstract class MVPBasePresenter<T>{...}

其實在Presenter抽象類里,用來處理View的泛型是與業務無關的,我們此前是做了一個View的抽象類來配合Presenter做弱引用處理,其實細想起來,這個View的角色沒必要使用抽象類,我們用一個IView基礎接口就可以滿足需要:

//基礎接口,不需要定義任何方法
public interface IMVPBaseView {}

我們的業務接口里,IView業務接口繼承這個基礎接口:

public interface CategoryContract {
    ...
    interface View implements IMVPBaseView{
    ...
    }
}

我們的Fragement可以恢復Google教科書那樣的簡潔:

public class TasksFragment extends Fragment implements IView{//實現的接口中包含基礎行為和業務邏輯
  ...
}

最終,我們的MVP結構是這樣的:
Model:接口注入(更靈活)或引用一個全局單例(更干凈)
View:IMVPBaseView(基礎行為)-> IContract.View(業務邏輯)-> XXFragment(V的具體實現)
Presenter:(MVPBasePresenter(基礎行為) + IContract.Presenter)-> XXPresenter(P的具體實現)
當然,在這種方式下,Presenter的創建、初始化、銷毀等行為,也還給了最終的業務Fragment(或Activity)。

演化4-Dagger

MVP里面其實有大量的依賴關系和注入行為,代碼會顯得比較復雜,而Dagger是一個專門處理依賴注入的框架,可以用配置的方式實現復雜的依賴關系,所以我們完全可以用Dagger來實現MVP
在Dagger(Dagger2)里,核心要素就是Module、Inject和Component,它們分別起這樣的作用:

  • Module:提供依賴,其實就是把我們此前用set或構造參數注入的依賴實例,改用module配置出來,由Dagger負責傳給要注入的類,比如把IView和數據Model注入到Presenter里,代碼形如:
//為presenter提供IView參數實例
@Module
public class TasksPresenterModule {
    private final TasksContract.View mView;
    public TasksPresenterModule(TasksContract.View view) {
        mView = view;
    }
    @Provides//提供參數的函數方法
    TasksContract.View provideTasksContractView() {
        return mView;
    }
}

//為presenter提供數據Model參數實例(google官方示例里又嵌套了幾層component)
@Singleton//要求dagger實現單例
@Component(modules = {TasksRepositoryModule.class, ApplicationModule.class})
public interface TasksRepositoryComponent {
    TasksRepository getTasksRepository();
}
  • Inject:指定依賴,就是說明某個屬性對象是需要用Module注入進來的,比如在Presenter里說明某個modle對象和某個view對象是需要dagger注入進來的,代碼形如:
//presenter類的參數改用Dagger注入
class TasksPresenter implements TasksContract.Presenter {
    private final TasksRepository mTasksRepository;
    private final TasksContract.View mTasksView;
    /**
     * Dagger strictly enforces that arguments not marked with {@code @Nullable} are not injected
     * with {@code @Nullable} values.
     */
    @Inject   //參數是需要注入的
    TasksPresenter(TasksRepository tasksRepository, TasksContract.View tasksView) {
        mTasksRepository = tasksRepository;
        mTasksView = tasksView;
    }
    ...
}

同樣,在Activity里也要用dagger注入persenter,代碼形如:

public class TasksActivity extends AppCompatActivity {
    @Inject TasksPresenter mTasksPresenter;//內部對象是需要注入的
    ...
}
  • Component:組裝器,做兩件事:1-把做好的Module對象作為參數提供給要注入的類,比如把Modle對象和IView對象實例化,作為Presenter的參數,完成Presenter的實例化;2-把完成注入和實例化的類,注入到當前類里,比如把完成實例化的Presenter注入到Activity里,代碼形如:
public class TasksActivity extends AppCompatActivity {
    @Inject TasksPresenter mTasksPresenter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        // Create the presenter
        DaggerTasksComponent.builder()
                //component會做出presenter需要的兩個參數
                .tasksRepositoryComponent(((ToDoApplication) getApplication()).getTasksRepositoryComponent())
                .tasksPresenterModule(new TasksPresenterModule(tasksFragment))
                .build()//構造出Presenter的實例
                .inject(this);//把Presenter注入到當前Activity中
        ...
    }
    ...
}

Dagger只是用注解來配置依賴關系,編譯時還是用工廠類和傳參等形式實現的依賴注入,例如,針對上述代碼,Dagger的apt插件會在編譯時把它轉成形如這樣的代碼:

...
//生成的Component類里,Module工廠類實現Module的實例化
this.provideTasksViewProvider = MainModule_ProvideTasksViewFactory.create(builder.mainModule);
...
//生成的Component類里,Presenter工廠類實現Presenter的實例化
mainPresenterProvider = MainPresenter_Factory.create(provideTasksRepositoryProvider,provideTasksViewProvider);
...
//生成的Activity的Injector類里,用構造參數實現依賴注入
this.mainPresenterProvider = mainPresenterProvider;

這樣用Dagger實現的MVP,最開始會有點別扭,因為類之間的注入關系好像不像直接代碼實現那樣熟悉,但習慣之后,你會發現這么幾個好處:
1.基于JSR330的穩定和標準的依賴注入方法
2.依賴關系是配置化的,代碼可讀性更強,也容易聚焦業務
3.可以通過注解實現全局單例

演化5-Kotlin的引入

作為基礎通用框架,我們必須有一個Kotlin的版本,當然,不同的演化版本,會有不同的寫法,如果參照演化3的版本,對應的Kotlin版本形如:

//基礎IMVPView接口
interface IMvpView {
}
//基礎MvpPresenter抽象類
abstract class MvpPresenter<T:IMvpView> {
    protected var mViewRef:WeakReIference<T>?=null

    fun attachView(view:T){
        mViewRef= WeakReference(view)
    }
    fun detachView(){
        if(mViewRef!= null){
            mViewRef!!.clear()
            mViewRef=null
        }
    }
    val view:T? get() = mViewRef!!.get()
}
//業務邏輯接口
interface ICatContract {
    interface View<Presenter>{
        fun refreshUI()
    }
    interface Presenter{
        fun doInitPage()
    }
}
//業務Presenter
class CatPresenter : MvpPresenter<CatActivity>(),ICatContract.Presenter {
    override fun doInitPage() {
    }
}
//業務Activity
class CatActivity : AppCompatActivity(),MvpView,ICatContract.View<CatPresenter> {
    val TAG: String = "CatActivity"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)
    }
    override fun refreshUI() {
    }
}

總結

MVP作為一個基礎型的結構,核心作用在于輔助我們實行良好的可讀性和可維護性,我們可以為一個Presenter提供多種View的實現(例如,一個業務可以同時有全屏Activity和對話框Activity兩種形式,分別提供給不同的業務環節,背后卻使用同一個Presenter),也可以為一個Presenter提供不同的數據Model(例如,在兩個根據后臺數據動態繪制界面的Activity實例中,業務邏輯一致,可以使用同一種Presenter,但數據內容不同,就可以使用兩個分別注入了不同Model的Presenter實例)
MVP里有Passive View(Presenter通過View的接口操作View,并作為主要的業務驅動方)和Supervisor Controller(Presenter負責大量復雜的View邏輯)兩種衍生,
MVP還是一個開放性的結構,你可以根據自己的需要,去規避某些缺陷,或取得某些優勢,如何去演化一個適合自己需求的MVP框架,一方面滿足需求,一方面保持靈活,完全看自己的發揮了

關于MVVM

MVP的結構比較通透明了,不過其中的View總是要寫一些業務邏輯相關的代碼,比如操縱Presenter,處理生命周期,實例化Model對象等,如果需要更進一步,把View的角色限定為純粹的UI,不做任何業務邏輯,不涉及任何數據,就需要用到MVVM模式了。
在MVVM模式里,不再有Presenter,用ViewModel來處理業務邏輯,ViewModel不處理UI,而View只負責UI,與ViewModel建立數據綁定關系,通過databinding自動實現UI和Model之間的數據操作。
技術細節推薦閱讀如何構建Android MVVM應用程序

引用

Github Google android-architecture
Android App的設計架構:MVC,MVP,MVVM與架構經驗談

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容