Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> MVP模式在Android開發中的最佳實踐

MVP模式在Android開發中的最佳實踐

編輯:關於Android編程

這篇文章拖了好久了,一直存在草稿箱裡沒有繼續寫,趁今天有空,撸撸完。

回想一下,你剛剛學習Android的時候,總會看到一些書上寫著,Android使用的是MVC模式,Activity就是一個Controller,或許那個時候,你沒有什麼深刻的體會。隨著經驗的積累。你發現,Activity既是Controller,掌管著許許多多的業務邏輯,同時它也作為View的一部分,控制著視圖層的顯示。久而久之,這個Controller便顯得過於重,職責不再那麼單一。

於是,再後來,為了使Activity的職責更加單一,便出現了MVP,MVVM等模式,只能說各有各的優點,沒有誰對誰錯,一個模式有另一個模式不具有的特點,同時也不具備另一個模式具有的特點,架構的選擇永遠是根據業務的復雜程度來進行的。MVC有其特點,就是寫代碼簡單啊,但是其缺點也很明顯,業務復雜起來後,Activity顯得過於龐大不是特別好維護。至於MVVM,個人是十分排斥這種模式的,為什麼呢,在XML中寫數據綁定的代碼顯得有點蛋疼,從而使得xml的職責不是那麼單一,在我看來,xml用來作為View再好不過了,不必摻和其他任何元素進來,這樣顯得“不干淨”。而MVP呢,我覺得在Android開發中,MVP是一個值得考慮的模式,它既沒有MVVM那樣,在xml中寫數據綁定的代碼,xml依然還是原來的配方,也沒有MVC那樣,擁有一個臃腫的Controller,取而代之的是更加清晰的分層,職責更加單一,當然,優點背後必然有缺點,相信用過MVP的都知道有什麼缺點,那就是接口的定義會暴增。

那麼什麼是MVP模式呢?

M即Model,what to show? 也就是顯示在UI上的數據,至於數據怎麼來,數據庫,網絡等等渠道,都是屬於這一層

V即View,how to show?也就是怎麼顯示數據,在Android中,通常是使用xml定義這個view,一般View中會持有Presenter的引用。

P即Presenter,Presenter扮演著中間聯系人的作用,就好比MVC中的Controller,通常來說,Presenetr中一般會持有View和Model的引用。

這三者的聯系如下圖所示:

這裡寫圖片描述

那麼問題來了,該如何實現MVP模式呢?這裡介紹一個開源庫MZ喎?/kf/ware/vc/" target="_blank" class="keylink">vc2J5o6xnaXRodWK12Na3aHR0cHM6Ly9naXRodWIuY29tL3NvY2tlcXdlL21vc2J5PC9wPg0KPHA+sb7Gqs7E1cKyu7bUuMO/4rXEvt/M5cq1z9bX97fWzvajrMjnufu21Mq1z9a40NDLyKS1xL/J0tTUxLbB1LTC66Ossc++udS0wuvWrsewo6zBy87ew9jD3KGj1NrKudPDx7CjrM/IvNPI67bUuMO/4rXE0sDAtTwvcD4NCjxwcmUgY2xhc3M9"brush:java;"> dependencies { compile 'com.hannesdorfmann.mosby:mvp:2.0.1' compile 'com.hannesdorfmann.mosby:viewstate:2.0.1' }

現在假設我們實現一個登陸功能,原來的MVC方式就是先定義好xml,然後直接在Activity中書寫各種業務邏輯,導致Activity越來越龐大,而使用了MVP之後,Activity會顯得十分干淨。

XML的定義這裡就不再貼了,兩個輸入框(賬號和密碼),一個登陸按鈕。

首先,我們需要一個與服務器交互的接口,為了簡單起見,我們在本地進行模擬,如果賬號密碼都是admin,則登陸成功,如果賬號密碼都是server,其他情況都返回賬號或密碼錯誤。理論上,這個需要在子線程中發起請求,再通過UI線程回調,這一步也省略,直接在主線程中判斷並回調,由於是本地模擬,不會產生任何卡頓,實際使用時需嚴格按照子線程請求主線程回調。

public interface Listener {
    void onSuccess(T t);

    void onFailure(int code);
}
public class LoginApi {
    public static void login(String username, String password, Listener listener) {
        if (username.equals("admin") && password.equals("admin")) {
            listener.onSuccess(null);
        } else if (username.equals("server") && password.equals("server")) {
            listener.onFailure(LoginView.SERVER_ERROR);
        } else {
            listener.onFailure(LoginView.USERNAME_OR_PASSWORD_ERROR);
        }
    }
}

業務邏輯的接口定義好了,這個LoginApi可以認為是Model層,接下來我們需要定義和Login相關的View,Presenter。

首先定義一個LoginView接口繼承MvpView接口,由於登錄的接口有兩種情況,一種是登錄成功,一種是登錄失敗,而登錄失敗的情況又有多種,於是需要通過一個狀態碼進行區分,於是LoginView中的接口就產生了。這裡我們直接將各種錯誤狀態定義在了LoginView中,實際使用時建議定義在一個常量類中進行統一管理。

public interface LoginView extends MvpView {

    public static final int USERNAME_OR_PASSWORD_EMPTY = 0x01;
    public static final int USERNAME_OR_PASSWORD_ERROR = 0x02;
    public static final int SERVER_ERROR = 0x03;

    void onLoginSuccess();

    void onLoginFailure(int code);
}

然後定義一個LoginPresenter類繼承MvpBasePresenter,泛型參數是LoginView,在裡面調用LoginApi的接口並將接口返回。

public class LoginPresenter extends MvpBasePresenter {

    public void login(final String username, final String password) {
        if (username == null || username.equals("")) {
            LoginView view = getView();
            if (view != null) {
                view.onLoginFailure(LoginView.USERNAME_OR_PASSWORD_EMPTY);
                return;
            }
        } else if (password == null || password.equals("")) {
            LoginView view = getView();
            if (view != null) {
                view.onLoginFailure(LoginView.USERNAME_OR_PASSWORD_EMPTY);
                return;
            }
        }
        Listener listener = new Listener() {
            @Override
            public void onSuccess(String str) {
                LoginView view = getView();
                if (view != null) {
                    view.onLoginSuccess();
                }
            }

            @Override
            public void onFailure(int code) {
                if (code == LoginView.USERNAME_OR_PASSWORD_ERROR) {
                    LoginView view = getView();
                    if (view != null) {
                        view.onLoginFailure(LoginView.USERNAME_OR_PASSWORD_ERROR);
                    }
                } else {
                    LoginView view = getView();
                    if (view != null) {
                        view.onLoginFailure(LoginView.SERVER_ERROR);
                    }
                }
            }
        };
        LoginApi.login(username, password, listener);
    }
}

最後便是讓Activity實現LoginView接口,實現LoginView中定義的接口,此外,還需要繼承MvpActivity,泛型參數是LoginView和LoginPresenter,並實現抽象方法createPresenter()返回LoginPresenter,而在LoginView中定義的兩個接口onLoginSuccess和onLoginFailure中,全都是UI相關的代碼,整個Activity中不再有業務邏輯的代碼,職責也就單一了。

public class LoginActivity extends MvpActivity implements View.OnClickListener, LoginView {
    private EditText etAccount;
    private EditText etPassword;
    private Button btnLogin;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        etAccount = (EditText) findViewById(R.id.accout);
        etPassword = (EditText) findViewById(R.id.password);
        btnLogin = (Button) findViewById(R.id.login);
        btnLogin.setOnClickListener(this);

    }

    @NonNull
    @Override
    public LoginPresenter createPresenter() {
        return new LoginPresenter();
    }


    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.login:
                onLogin();
                break;

        }
    }

    private void onLogin() {
        String username = etAccount.getText().toString();
        String passowrd = etPassword.getText().toString();
        getPresenter().login(username, passowrd);
    }


    @Override
    public void onLoginSuccess() {
        Toast.makeText(this, "登陸成功", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onLoginFailure(int code) {
        switch (code) {
            case LoginView.USERNAME_OR_PASSWORD_EMPTY:
                Toast.makeText(this, "賬號或密碼不能為空", Toast.LENGTH_SHORT).show();
                break;
            case LoginView.USERNAME_OR_PASSWORD_ERROR:
                Toast.makeText(this, "賬號或密碼錯誤", Toast.LENGTH_SHORT).show();
                break;
            case LoginView.SERVER_ERROR:
                Toast.makeText(this, "服務器錯誤", Toast.LENGTH_SHORT).show();
                break;
        }

    }
}

特別需要注意的是,在Presenter中引用View時,一定要判斷是否非空,因為這個View是WeakReference弱引用,不進行判斷的話會產生空指針異常。這是這個框架不好的地方,需要多次重復判空。

以上是這個框架最基礎的用法,實際使用時我們一般不會這麼直接使用它的類,一般來說,我們會定義各種Base類,比如BaseView,BasePresenter,BaseActivity,BaseFragment;從而將各種公共的方法都放著裡面,減少冗余。如果你要引用這個框架,實際使用時稍微注意一下這個問題就可以了。

此外,Mosby還有一個LCE模塊,什麼是LCE模塊呢,其實就是Loading-Content-Error的全稱,主要用於數據的加載,顯示燈作用,它體現在一個MvpLceView這個接口上以及具體的實現MvpLceActivity和MvpLceFragment上,該接口的定義如下。

public interface MvpLceView extends MvpView {

  /**
   * Display a loading view while loading data in background.
   * The loading view must have the id = R.id.loadingView
   *
   * @param pullToRefresh true, if pull-to-refresh has been invoked loading.
   */
  public void showLoading(boolean pullToRefresh);

  /**
   * Show the content view.
   *
   * The content view must have the id = R.id.contentView
   */
  public void showContent();

  /**
   * Show the error view.
   * The error view must be a TextView with the id = R.id.errorView
   *
   * @param e The Throwable that has caused this error
   * @param pullToRefresh true, if the exception was thrown during pull-to-refresh, otherwise
   * false.
   */
  public void showError(Throwable e, boolean pullToRefresh);

  /**
   * The data that should be displayed with {@link #showContent()}
   */
  public void setData(M data);

  /**
   * Load the data. Typically invokes the presenter method to load the desired data.
   *

* Should not be called from presenter to prevent infinity loops. The method is declared * in * the views interface to add support for view state easily. *

* * @param pullToRefresh true, if triggered by a pull to refresh. Otherwise false. */ public void loadData(boolean pullToRefresh); }

該接口中定義了5個方法,

showLoading 用於顯示加載數據時的動畫,比如進度條 showError 用於顯示加載數據失敗的內容 setData 當數據加載成功時,將數據進行賦值,在調用showContent之前進行調用 loadData 加載數據,這個方法一般是放著Activity或者Fragment中進行調用的 showContent 數據加載成功時顯示

除此之外,我們還要使用MvpLceActivity或者MvpLceFragment,還要在xml中定義相關的View,比如errorView,contenView等等。

現在我們來實踐一下,以顯示一個新聞列表為例。

首先定義布局,在布局中需要聲明errorView,loadingView,contentView這幾個id

<code class=" hljs xml"><framelayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">

    <!--{cke_protected}{C}%3C!%2D%2D%20Loading%20View%20%2D%2D%3E-->
    <progressbar android:id="@+id/loadingView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:indeterminate="true">

    <!--{cke_protected}{C}%3C!%2D%2D%20Content%20View%20%2D%2D%3E-->
    <android.support.v4.widget.swiperefreshlayout android:id="@+id/contentView" android:layout_width="match_parent" android:layout_height="match_parent">

        <android.support.v7.widget.recyclerview android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent">

    </android.support.v7.widget.recyclerview></android.support.v4.widget.swiperefreshlayout>


    <!--{cke_protected}{C}%3C!%2D%2D%20Error%20view%20%2D%2D%3E-->
    <textview android:id="@+id/errorView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="error">

</textview></progressbar></framelayout></code>

定義實體類,並添加構造函數和getter,setter方法

public class News {
    private String title;
    private String desprition;

    public News(String title, String desprition) {
        this.title = title;
        this.desprition = desprition;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDesprition() {
        return desprition;
    }

    public void setDesprition(String desprition) {
        this.desprition = desprition;
    }

    @Override
    public String toString() {
        return "News{" +
                "title='" + title + '\'' +
                ", desprition='" + desprition + '\'' +
                '}';
    }
}

定義View層接口,空接口,繼承MvpLceView即可

public interface NewsView extends MvpLceView>{
}

定義Presenter層,調用Model層方法獲取數據源,在使用getView之前,一定要調用isViewAttached()方法或者使用getView!=null進行判空。不然極有可能產生空指針異常,在onSuccess中,調用view層的setData和showContent進行數據的顯示,在onFaliure中則調用showError顯示數據加載失敗。

public class NewsPresenter extends MvpBasePresenter {
    public void loadNews(final boolean pullToRefresh) {
        if (isViewAttached()) {
            getView().showLoading(pullToRefresh);
        }
        Listener> listener=new Listener>() {
            @Override
            public void onSuccess(List news) {
                if (isViewAttached()) {
                    getView().setData(news);
                    getView().showContent();
                }
            }
            @Override
            public void onFailure(int code) {
                if (isViewAttached()) {
                    getView().showError(new Exception("msg:"+code), pullToRefresh);
                }
            }
        };

        NewsApi.loadNews(pullToRefresh,listener);
    }
}

編寫接口方法,這裡同樣采用模擬,不過為了有加載動畫等效果的顯示,這裡在子線程中進行模擬,之後切回主線程,並且,為了達到服務器錯誤的模擬效果,使用了一個隨機數,當隨機數為奇數時則返回獲取數據失敗的場景

public class NewsApi {

    private static Handler handler = new Handler(Looper.getMainLooper());
    private static Random random = new Random();

    public static void loadNews(final boolean pullToRefresh, final Listener> listener) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                final List list = new ArrayList();
                News news1 = new News("標題1", "描述描述描述描述描述描述描述描述描述描述描述描述1");
                News news2 = new News("標題2", "描述描述描述描述描述描述描述描述描述描述描述描述2");
                News news3 = new News("標題3", "描述描述描述描述描述描述描述描述描述描述描述描述3");
                News news4 = new News("標題4", "描述描述描述描述描述描述描述描述描述描述描述描述4");
                News news5 = new News("標題5", "描述描述描述描述描述描述描述描述描述描述描述描述5");
                News news6 = new News("標題6", "描述描述描述描述描述描述描述描述描述描述描述描述6");

                list.add(news1);
                list.add(news2);
                list.add(news3);
                list.add(news4);
                list.add(news5);

                if (pullToRefresh) {
                    list.add(news6);
                }


                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        if (listener != null) {
                            listener.onFailure(1);
                            int i = random.nextInt(100);
                            if (i % 2 == 0) {
                                listener.onSuccess(list);
                            } else {
                                listener.onFailure(1000);
                            }
                        }

                    }
                });
            }
        }).start();

    }

}

對應的Activity則是繼承了MvpLceActivity,重寫抽象方法,理論上來說showContent和showError是不需要重寫的,但是這裡使用了SwipeRefreshLayout,需要將加載的那個圓圈給隱藏掉,需要重寫這兩個方法,調用setRefreshing設為false;getErrorMessage方法返回的字符串類型便是用來顯示在errorView上的,當不是下拉刷新時,則直接顯示在errorView上,否則,使用Toast進行彈出。setData方法就是數據獲取成功後對數據源進行使用,比如設置到adapter並通知數據源改變。loadData方法則調用presenter中的方法進行加載即可

public class NewsActivity extends MvpLceActivity, NewsView, NewsPresenter> implements NewsView, SwipeRefreshLayout.OnRefreshListener {
    private RecyclerView recyclerView;
    private NewsAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_news);
        adapter = new NewsAdapter();
        contentView.setOnRefreshListener(this);
        recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(adapter);
        loadData(false);
    }

    @NonNull
    @Override
    public NewsPresenter createPresenter() {
        return new NewsPresenter();
    }

    @Override
    public void showContent() {
        super.showContent();
        contentView.setRefreshing(false);
    }

    @Override
    public void showError(Throwable e, boolean pullToRefresh) {
        super.showError(e, pullToRefresh);
        contentView.setRefreshing(false);
    }

    @Override
    protected String getErrorMessage(Throwable e, boolean pullToRefresh) {
        return "發生了錯誤";
    }

    @Override
    public void setData(List data) {
        adapter.setNews(data);
        adapter.notifyDataSetChanged();
    }

    @Override
    public void loadData(boolean pullToRefresh) {
        presenter.loadNews(pullToRefresh);
    }

    @Override
    public void onRefresh() {
        contentView.setRefreshing(true);
        loadData(true);
    }
}

adapter就不貼了,比較簡單。

最終的效果如下

這裡寫圖片描述源碼。

最後,貼上全部代碼。

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