Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 安卓實踐開發之MVP一步步實現到高級封裝

安卓實踐開發之MVP一步步實現到高級封裝

編輯:關於Android編程

在上家干了快2年辭職後在家休息了快一個月了,說實在的不上班的感覺爽(睡覺睡到自然醒,游戲玩到手抽筋)。哈哈,又是快到一年過中秋的時候了,好久沒有更新博客了,今天順便撸一篇。

前言

話說MVP的模式已經問世好幾年了,為什麼很多公司還是不願意接受呢?說實在的我就還是喜歡自己的mvc,不喜歡看見mvp龐大的架構,所以前公司的項目呢也不曾使用過mvp(同事也不接受這種模式),畢竟項目架構不是特別復雜的話使用mvp顯示不出他的優勢,相反給人的感覺是實現一個界面多出了很多的代碼。

 

然而現在最火的應該是mvvm模式了畢竟是數據雙向綁定,讓我想起了angularjs估計效率很高,但是mvp我們還是要知道的(畢竟還是不錯的)。

mvp產生的原因

原生的 MVC 框架遇到大規模的應用,就會變得代碼難讀,不好維護,無法測試的囧境。因此,Android 開發方面也有很多對應的框架來解決這些問題。

構建框架的最終目的是增強項目代碼的 可讀性 , 維護性 和 方便測試 ,如果背離了這個初衷,為了使用而使用,最終是得不償失的

從根本上來講,要解決上述的三個問題,核心思想無非兩種:一個是 分層 ,一個是 模塊化 。兩個方法最終要實現的就是解耦,分層講的是縱向層面上的解耦,模塊化則是橫向上的解耦。下面我們來詳細討論一下 Android 開發如何實現不同層面上的解耦。

解耦的常用方法有兩種: 分層 與 模塊化

橫向的模塊化對大家來可能並不陌生,在一個項目建立項目文件夾的時候就會遇到這個問題,通常的做法是將相同功能的模塊放到同一個目錄下,更復雜的,可以通過插件化來實現功能的分離與加載。

縱向的分層,不同的項目可能就有不同的分法,並且隨著項目的復雜度變大,層次可能越來越多。

對於經典的 Android MVC 框架來說,如果只是簡單的應用,業務邏輯寫到 Activity 下面並無太多問題,但一旦業務逐漸變得復雜起來,每個頁面之間有不同的數據交互和業務交流時,activity 的代碼就會急劇膨脹,代碼就會變得可讀性,維護性很差。

總的來說:項目大了我們便於修改和測試,代碼重用性高。

MVC和MVP的區別?

\
從上圖我們知道mvc和mvp的結構了:
MVP 是從經典的模式MVC演變而來,它們的基本思想有相通的地方:Controller/Presenter負責邏輯的處理,Model提供數據,View負 責顯示。作為一種新的模式,MVP與MVC有著一個重大的區別:在MVP中View並不直接使用Model,它們之間的通信是通過Presenter (MVC中的Controller)來進行的,所有的交互都發生在Presenter內部,而在MVC中View會從直接Model中讀取數據而不是通過 Controller。

在MVC裡,View是可以直接訪問Model的!從而,View裡會包含Model信息,不可避免的還要包括一些業務邏輯。 在MVC模型裡,更關注的Model的不變,而同時有多個對Model的不同顯示,及View。所以,在MVC模型裡,Model不依賴於View,但是 View是依賴於Model的。不僅如此,因為有一些業務邏輯在View裡實現了,導致要更改View也是比較困難的,至少那些業務邏輯是無法重用的。

1、模型與視圖完全分離,我們可以修改視圖而不影響模型
2、可以更高效地使用模型,因為所以的交互都發生在一個地方——Presenter內部
3、我們可以將一個Presener用於多個視圖,而不需要改變Presenter的邏輯。這個特性非常的有用,因為視圖的變化總是比模型的變化頻繁。
4、如果我們把邏輯放在Presenter中,那麼我們就可以脫離用戶接口來測試這些邏輯(單元測試)

未使用MVP程序是這樣的:

使用MVP後是這樣的:

MVP入門的代碼實現(入門級別的實現)

網上很多的例子都是使用登錄模塊來進行mvp的講解,但是我們只要知道MVP的流程不管怎麼玩還是可以的。

Model層

package com.losileeya.mvpsimpledemo.model;
import android.os.Handler;
import android.os.Looper;
/**

- User: Losileeya ([email protected])
- Date: 2016-09-09
- Time: 11:33
- 類描述:
   *
- @version :
   */
  public class UserModel {
   private Handler handler=new Handler(Looper.getMainLooper());//主線程handler一步處理
   /**
  - model層業務邏輯處理
  - @param username   用戶名
  - @param password   密碼
  - @param callBack   結果回調
    */
public  void  login(final String username,final  String password,final  CallBack callBack){
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            if(username.equals("123")&&password.equals("123"))
                callBack.onSuccess();
            else
                callBack.onFilure("帳號或者密碼錯誤");
        }
    },2000);
  }
    }

代碼很簡單就是一個登錄方法的處理,正常情況就裡就是調服務器登錄接口,我們這裡就用延時操作來模擬網絡請求。這裡對用戶名密碼做了一個判斷,如果用戶名為123密碼為123就去成功的回調,否則返回“帳號或密碼錯誤”。

public interface CallBack {
  /**
 * model處理邏輯:成功回調
 */
void onSuccess();
/**
 * model處理邏輯:失敗回調
 */
void onFilure(String fail);
  }

CallBack就是我們對model的邏輯的具體處理,提供這個插座去做你具體的實現邏輯。

View層

public interface LoginView {
  /**
 * 顯示進度條
 * @param msg   進度條加載內容
 */
void showLoding(String msg);
/**
 * 隱藏進度條
 */
void  hideLoding();
/**
 * 顯示登錄的結果
 * @param result
 */
void showResult(String result);
/**
 * 顯示加載錯誤
 * @param err 錯誤內容
 */
void showErr(String err);
/**
 * 獲得界面上用戶名的值
 * @return
 */
String getUsername();
/**
 * 獲得界面上密碼的值
 * @return
 */
String getPassword();
  }

我們把登錄模塊的ui邏輯作為一個接口的形式來處理,方便我們的activity處理的時候來進行界面顯示,以及把它交給Presenter層來處理具體的ui展示邏輯。

一部份是給Presenter提供數據的(View->Presenter)

getPassword

getUsername

另一部份是從Presenter接收數據用來顯示的(Presenter->View)

hideLoading

showResult

showErr

showLoading

說白了就是數據與UI的交互。

Presenter

這一層就是mvp的核心,把view層和model層建立起來聯系:

public class LoginPresenter {
  private UserModel userModel;//model層具體實現類
  private LoginView loginView;//loginView接口
  public LoginPresenter(UserModel userModel, LoginView loginView) {
    this.userModel = userModel;
    this.loginView = loginView;
}
public void login(){
    loginView.showLoding("正在登錄中...");//loginView的ui邏輯處理
    userModel.login(loginView.getUsername(), loginView.getPassword(), new CallBack() {
        @Override
        public void onSuccess() {
            loginView.hideLoding();
            loginView.showResult("登錄成功");
        }
         @Override
        public void onFilure(String fail) {
            loginView.hideLoding();
            loginView.showErr(fail);
        }
    });
}}

就一個登錄方法,在login方法被調用時,LoginPresenter會從loginView獲取到用戶輸入的用戶名和密碼,並以此為參數調用userModel的login方法,然後通過回調把結果返回給loginView。

Activity(View 層也就是loginView的實現)

public class LoginActivity extends AppCompatActivity implements LoginView, View.OnClickListener {
  private LoginPresenter loginPresenter;
private ProgressDialog progressDialog;
private EditText username;
private EditText password;
private Button login;
  @Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login);
    initView();
    loginPresenter = initPresenter();//初始化presenter
}
public LoginPresenter initPresenter() {
    return new LoginPresenter(new UserModel(), this);
}
@Override
public void showLoding(String msg) {
    progressDialog.setMessage(msg);
    if(!progressDialog.isShowing())
         progressDialog.show();
}
@Override
public void hideLoding() {
   if(progressDialog.isShowing())
       progressDialog.dismiss();
}
@Override
public String getUsername() {
    return username.getText().toString().trim();
}
@Override
public String getPassword() {
    return password.getText().toString().trim();
}
@Override
public void showResult(String result) {
    Toast.makeText(this, result, Toast.LENGTH_SHORT).show();
}
@Override
public void showErr(String err) {
    Toast.makeText(this, err, Toast.LENGTH_SHORT).show();
}
  @Override
protected void onDestroy() {
    if (loginPresenter != null) {
        loginPresenter = null;
    }
    super.onDestroy();
}
private void initView() {
    username = (EditText) findViewById(R.id.username);
    password = (EditText) findViewById(R.id.password);
    login = (Button) findViewById(R.id.login);
    progressDialog =new ProgressDialog(this);
    login.setOnClickListener(this);
}
  @Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.login:
             loginPresenter.login();//loginPresenter的login
            break;
    }
}
  }

代碼可以看出邏輯清晰,只需要提供一下view 層的方法需要我們傳入一個Presenter來處理登錄的邏輯。我們只需要記住要初始化Presenter並且在view銷毀的時候釋放資源。

上面代碼我們可以看出還是有很多需要優化的地方,接下來我們就來重新封裝我們的mvp。

MVP的高級封裝(從入門到放棄)

鑒於裝逼和簡化解耦的需求,我們需要封裝一下mvp的架構,容我喝口奶壓壓驚。
這裡寫圖片描述
當然裝逼我們就擺脫不了泛型的支持,哈哈,泛型真的是封裝中的利器,是應該需要好好的學習。

Model層的封裝

我們 以前都是直接使用UserModel類,這樣呢不利於擴展,所以呢我們先抽取一個IUserModel的接口,便於我們對代碼邏輯更加清晰的梳理,也對代碼的維護性有好處。

public interface IUserModel {
  /**
 *  登錄邏輯處理
 * @param username   用戶名
 * @param password   密碼
 * @param callBack   結果回調
 */
void login(String username,String password,CallBack  callBack);
  }

我們只需要簡單的抽取成接口好了,加個代碼的實現是不是看起來更加的邏輯清楚:

public class UserModel implements IUserModel {
    private Handler handler=new Handler(Looper.getMainLooper());//主線程handler一步處理

    /**
     * model層業務邏輯處理
     * @param username   用戶名
     * @param password   密碼
     * @param callBack   結果回調
     */
    @Override
    public void login(final String username, final String password, final CallBack callBack) {
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if(username.equals("123")&&password.equals("123"))
                    callBack.onSuccess();
                else
                    callBack.onFailure("帳號或者密碼不正確");
            }
        }, 2000);
    }
}

請注意:我們這裡為了刷新Ui所以都使用了handler,這裡是處理數據的邏輯,項目中換成其他的網絡框架也好,handler在這裡只是業務的需要。

View層

我們就給view提供了一個接口,顯然是可以繼續擴展的,因為每次一個view我們就得寫一個接口,而且view的顯示還是有一些公用的方法,如果我們不去抽取就顯得代碼又重復無法很好的體現代碼的復用性和低耦合。

在這裡 我需要新建一個base的包,用於放一下通用的類和接口來降低耦合度:

public interface BaseMvpView {
  /**
 * 顯示進度條
 * @param msg   進度條加載內容
 */
void showLoding(String msg);
  /**
 * 隱藏進度條
 */
void hideLoding();
  /**
 * 顯示加載錯誤
 * @param err 錯誤內容
 */
void showErr(String err);
  }

再來看下LoginView的寫法:

public interface LoginView extends BaseMvpView{
  /**
 * 獲得界面上用戶名的值
 * @return
 */
String getUsername();
  /**
 * 獲得界面上密碼的值
 * @return
 */
String getPassword();
  /**
 * 顯示登錄的結果
 * @param result
 */
void showResult(String result);
  }

是不是顯得清爽多了。

Presenter層(最難的)

由於presenter層是聯系view和model層,所以封裝相對來說困難的多。入門的代碼在處理Presenter的時候還是有很多的問題的,比如view銷毀了,然後presenter層沒有銷毀還在調用view層的方法就會導致空指針異常。

所以我們就的做如下修改:

public interface Presenter {
/**
 * presenter和對應的view綁定
 * @param mvpView  目標view
 */
void attachView(V mvpView);
/**
 * presenter與view解綁
 */
void detachView();
}

寫一個基礎接口Presenter它接收BaseMvpView的子類,有兩個方法,attachView,detachView分別用於與MvpView建立與斷開連接,然後我們再來寫一個BaseMvpPresenter來具體操作。

public class BaseMvpPresenter implements Presenter {
private  V mvpView;
@Override
public void attachView(V mvpView) {
    this.mvpView = mvpView;
}
@Override
public void detachView() {
     mvpView = null;
}
/**
 * 判斷 view是否為空
 * @return
 */
public  boolean isAttachView(){
    return mvpView != null;
}
/**
 * 返回目標view
 * @return
 */
public  V getMvpView(){
    return mvpView;
}
/**
 * 檢查view和presenter是否連接
 */
public void checkViewAttach(){
    if(! isAttachView()){
       throw  new MvpViewNotAttachedException();
    }
}
/**
 * 自定義異常
 */
   public static   class  MvpViewNotAttachedException extends RuntimeException{
       public  MvpViewNotAttachedException(){
        super("請求數據前請先調用 attachView(MvpView) 方法與View建立連接");
    }
}
}    

這個類用來判斷view是否和presenter建立連接,並且提供方法讓我們獲得目標view便於presenter來處理相關邏輯。

具體的LoginPresenter我們也就好實現了:

為了便於梳理項目流程,我們還是提供一個基礎接口:

  public interface ILoginPresenter {
    /**
     * presenter login
     */
    void login();
    }

以上代碼只不過是為了讓我們更加清晰的看清業務邏輯。下面才是LoginPresenter:

public class LoginPresenter extends BaseMvpPresenter implements ILoginPresenter{
  private UserModel userModel;//model層具體實現類
  public LoginPresenter(UserModel userModel) {
    this.userModel = userModel;
}
  @Override
public void login() {
    checkViewAttach();//檢查是否綁定
    final LoginView loginView=getMvpView();//獲得LoginView
    loginView.showLoding("正在登錄中...");//loginView的ui邏輯處理
    userModel.login(loginView.getUsername(), loginView.getPassword(), new CallBack() {
        @Override
        public void onSuccess() {
            loginView.hideLoding();
            loginView.showResult("登錄成功");
        }
             @Override
        public void onFailure(String fail) {
            loginView.hideLoding();
            loginView.showResult(fail);
        }
    });
} }

邏輯很簡單,調用login方法,裡面我們 需要先檢查view和presenter是否已經建立連接,建立聯系後我們再來獲取目標的view來進行view邏輯的操作,同時我們通過model層來進行相關的業務處理邏輯,並且把處理結果傳遞給ui層。

然後我們初步的優化已經差不多了:(Activity具體使用)

在activity的onCreate方法中我們記得實例化presenter
presenter = new LoginPresenter(new UserModel());
在activity的onCreate方法中我們記得綁定View
presenter.attachView(this);//這裡與View建立連接

3.在activity的onDestroy方法中presenter

presenter.detachView();//這裡與View斷開連接

顯然我們還有優化的空間。


話說當Activity會在很多情況下被系統重啟:當用戶旋轉屏幕、在後台時內存不足、改變語言設置、attache 一個外部顯示器等。

我們如何控制presenter的生命周期呢:

1.將Presenter保存在一個地方,再次onCreate時還原
我見到的最多的就是這個方案,不過具體的實現方法千差萬別。
最簡單最naive的實現方式,就是在Activity/Fragment中保留對Presenter的靜態引用。這樣我們可以再手機旋轉時保留Presenter對象,但整個Applicaiton中只能有一個同樣的Activity/Fragment-Presenter實例組合。但當一個ViewPager中有多個相同的Fragment時,這種方法就行不通了。
2.另一個方式就是調用Fragment的setRetainInstance(true)方法。
設置Fragment的這個屬性可以保證Fragment不會被destroy,這樣Presenter就隨之被保留。但這種方式仍有局限,對一個子Fragment或是Activity這樣就不起作用。
3.最後一種方式就是使用單例緩存機制並通過標識符來存儲一個Presenter類的不同實例。
為了保留Presenter,Activity/Fragment需要在onSaveInstanceState()中傳遞Presenter實例的標識符。這裡的問題是如何實現這種邏輯以及何時將Presenter從單例緩存中移除。
4.通過Loader延長Presenter生命周期

Loader是Android框架中提供的在手機狀態改變時不會被銷毀的工具。Loader的生命周期是是由系統控制的,只有在向Loader請求數據的Activity/Fragment被永久銷毀時才會被清除,所以也不需要自己寫代碼來清空它。

一般Loader是用來在後台加載數據的,而且是用它的子類CursorLoader或AsyncTaskLoader,尤其是CursorLoader,直接就綁定了Content Provider和數據庫。當然如果寫個類繼承Loader基類的話也不需要開啟後台線程。
自定義loader:

public class PresenterLoder

extends Loader

{ private final PresenterFactory

factory; private P presenter; public PresenterLoder(Context context, PresenterFactory

factory) { super(context); this.factory = factory; } ? /** * 在Activity的onStart()調用之後 */ @Override protected void onStartLoading() { if(presenter != null){ deliverResult(presenter);//會將Presenter傳遞給Activity/Fragment。 return; } forceLoad(); } ? /** * 在調用forceLoad()方法後自動調用,我們在這個方法中創建Presenter並返回它。 */ @Override protected void onForceLoad() { presenter = factory.crate();//創建presenter deliverResult(presenter); } ? /** * 會在Loader被銷毀之前調用,我們可以在這裡告知Presenter以終止某些操作或進行清理工作。 */ @Override protected void onReset() { presenter = null; } }

PresenterFactory:這個接口可以隱藏創建Presenter所需要的參數。通過這個接口我們可以調用各種構造器,這樣可以避免寫一堆PresenterLoader的子類來返回不同類型的Presenter。

public interface PresenterFactory

{ P crate();//創建presenter }

上訴代碼只是創建一個presenter而已。

Activity的實現

從初級的代碼我們知道activity裡面的登錄請求的進度條是可以抽取為通用的方法,這裡再次封裝一個BaseMvpActivity

public class BaseMvpActivity

,V extends BaseMvpView> extends AppCompatActivity implements BaseMvpView,LoaderManager.LoaderCallbacks

{ private final int BASE_LODER_ID = 1000;//loader的id值 private ProgressDialog progressDialog;//登錄進度條 protected P presenter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); progressDialog = new ProgressDialog(this);//實例化progressDialog getSupportLoaderManager().initLoader(BASE_LODER_ID,null,this);//初始化loader } @Override protected void onStart() { super.onStart(); presenter.attachView((V)this);//presenter與view斷開連接 } @Override public void showLoding(String msg) { progressDialog.setMessage(msg);//設置進度條加載內容 if (! progressDialog.isShowing())//如果進度條沒有顯示 progressDialog.show();//顯示進度條 } @Override public void hideLoding() { if (progressDialog.isShowing()) progressDialog.dismiss(); } @Override public void showErr(String err) { Toast.makeText(this, err, Toast.LENGTH_SHORT).show(); } @Override public Loader

onCreateLoader(int id, Bundle args) { return null; } @Override public void onLoadFinished(Loader

loader, P data) { presenter = data; } @Override public void onLoaderReset(Loader

loader) { presenter = null; } @Override protected void onDestroy() { presenter.detachView(); super.onDestroy(); } }

getSupportLoaderManager().initLoader(LOADER_ID, null, this);

onStart()方法執行時會創建Loader或重新連接到一個已經存在的Loader。在這裡我們在onLoadFinished()裡創建並傳遞Presenter對象。由於這些代碼都是同步的,所以當onStart()方法執行完後Presenter也可以正常工作了。

其他方法都差不多,從原來的activity搬過來的,你會疑問為什麼我們的onCreateLoader是空實現,因為這是我們的baseActivity肯定不會

創建具體的presenter得留給他的子類去實現。

LoginActivity的具體實現:

public class LoginActivity extends BaseMvpActivity implements LoginView, View.OnClickListener {
    private EditText username;
    private EditText password;
    private Button login;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        initView();
    }
    /**
     * 創建LoginPresenter實例
     * @param id
     * @param args
     * @return
     */
    @Override
    public Loader onCreateLoader(int id, Bundle args) {
        return new PresenterLoder<>(this, new PresenterFactory() {
            @Override
            public LoginPresenter crate() {
                return new LoginPresenter(new UserModel());
            }
        });
    }
    @Override
    public String getUsername() {
        return username.getText().toString().trim();
    }
    @Override
    public String getPassword() {
        return password.getText().toString().trim();
    }
    @Override
    public void showResult(String result) {
        Toast.makeText(this, result, Toast.LENGTH_SHORT).show();
    }
    private void initView() {
        username = (EditText) findViewById(R.id.username);
        password = (EditText) findViewById(R.id.password);
        login = (Button) findViewById(R.id.login);
        login.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.login:
                presenter.login();//登錄操作
                break;
        }
    }
}

看到沒有,我們在這裡實現了了onCreateLoader,在這裡產生LoginPresenter

哈哈,整個LoginActivity又變干淨了有木有,大部份Presenter,Loaders都放在了BaseActivity中。LoginActivity裡只要實現onCreateLoader就行了。

寫到這裡也就差不多了,再來看下效果吧:
這裡寫圖片描述

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved