MVP框架的演化

安卓開發高階技術分享發表於2019-01-17

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

 

 

 

各自分工如下:

  • 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:

 

 

  • 集中管理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高階技術大綱

附錄二;Android進階實戰技術視訊

 

獲取方式;

加Android進階群;701740775。即可前往免費領取。免費備註一下csdn

 

相關文章