MVP初步實踐

問題的引出

  對於一個基本的Android項目,我們在初學的時候的做法通常都是直接在xml中繪製界面,然後在對應的Activity中做一些響應操作。這樣做在一些demo演示的時候倒是沒什麼問題,但是一旦項目稍微有點規模,就會導致Activity變得臃腫。而對於一個成百上千行的Activity,當我們需要對界面進行修改的時候,會很難快速定位到對應修改的地方,並且由於所有的操作都在activity中,耦合度很高,當修改某一處的時候,可能會涉及到多個方面,從而需要修改多處。

  解決這個問題的方法就是將在activity中的操作進行分離,同時對於整個項目也進行功能模塊的分離。這樣的話,當我們需要修改某處的時候,只用修改對應的部分即可。因此就有了mvp模式了,雖然mvp模式也存在很多缺點,巨多的接口,但是對於一箇中小型項目,這點缺點還是可以忽略的。

MVP

  而MVP模式指的就是Model,View,Presenter這三者。也就是說MVP模式將集中在Activity中的操作拆分成了三個部分,由這三個部分共同完成一個操作。

  其中,model是獲取數據的部分,它只負責獲取數據,而不進行其他操作。View則是界面顯示的部分,同樣的,它只負責展示界面,不進行其他操作,由Activity/Fragment擔任。Presenter是連接model和View的部分,它是主要負責進行邏輯的處理。

MVp

  因此,一個完整的事件流程理應是這樣:在View中產生某個事件,然後由View轉交給Presenter,Presenter進行邏輯的處理後通知model獲取數據,在Model獲取完數據後回調給Presenter,然後presenter將數據進行處理後交給View去顯示。

在這個過程中,可以看出View與Model是完全沒有聯繫的,它們之間的數據通過Presenter進行互通。另外,Model獲取到的數據也是通過回調接口來提供給Presenter的。這樣,就可以將Model獨立出來,減少數據的耦合。
  所以他們的關係也應當如上圖所示,View與Presenter互相持有,可以互相通信。而Model只被Presenter持有,是單向通信的。

小結

  對於MVP而言,數據的獲取和界面的顯示是沒有關係的。也就是說,當我們需求進行變化的時候,就可以根據需求而只修改其中的一部分,而不用全部修改。
  另外,由於邏輯與界面的顯示是分離的,當我們測試的時候就可以不用對Activity進行註釋修改添加,而是可以直接新建一個Presenter繼承原Presenter,然後進行修改,極大的方便了我們測試。

  至於MVP的實現,也不是一成不變的,而是根據自己的習慣或者需求進行一些變動。畢竟MVP模式本身的引出就是爲了解耦,分割各個模塊的。

實現MVP

  對於一個應用程序,我們應當保持其界面的一致性,因此這裏先定義了三個接口,分別對應MVP的三個部分的行爲準則,這是三個接口中,定義的是每組MVP都應當擁有的功能,也就是說每組MVP都應當實現這些對應的方法。對於MVP,當然應該至少有三個類的實現,分別對應MVP的每個部分,稱爲一組MVP。
  既然說到了這裏,就有必要說一下分包的實現了。有些人習慣建立三個包Model、View、Presenter,然後將對應的文件分別放在這三個包中。還有些人習慣按照功能分包,然後將每個功能對應的MVP放在一起。這裏我就是採用的後者,按照功能分包,同時加入一個Contract來協同每組MVP的行爲。

包結構

  這裏,添加了一個Module,這個Module是專門用於放置MVP基礎的搭建的,叫做common。然後還有一個Module,叫做app,是具體的示例程序對應的Module。
包結構

具體實現

  首先是View。對於View而言,這裏只添加了兩個方法,進度條的顯示和隱藏。當進行某項操作的時候,應當顯示進度條,然後操作結束的時候隱藏,這樣才能使得應用更加友好。

public interface View {
    /**
     * 顯示進度條,用於提示正在加載
     */
    void showProgress();
    /**
     * 隱藏進度條
     */
    void hideProgress();
}

  接下來是model的接口,由於這裏model只是負責獲取數據的部分,它是獨立的。因而沒有什麼公共的方法,但是爲了後面的model的統一,這裏還是創建了一個model接口,只是其中並沒有方法而已。

public interface Model {
}

  最後就差Presenter了,而Presenter是model和view的連接者,它是需要同時持有model和view。但是對於View而言,它的生命週期很是複雜,我們需要當view創建的時候連接二者,而view銷燬的時候解開連接。否則的話當view銷燬,但是還被Presenter持有的時候,view就不會被回收,從而造成內存的泄露。那麼presenter就應當擁有解除關聯和判斷是否還在關聯的方法。

public interface Presenter {
    /**
     * 解除關聯
     */
    void detach();

    /**
     * 判斷View是否與當前的Presenter建立連接
     *
     * @return true爲已建立連接
     */
    boolean isAttach();

    /**
     * 檢查是否與View建立連接
     */
    void checkAttach();
}

  對於每個model,view,presenter,都應當實現以上的接口。而對於這種共有的方法,每個對應的類都去實現一遍顯然是不合適的,最好的做法是使用一個基類來實現這些方法,其餘的類只要繼承這個基類即可擁有這些方法了。同時也會因爲這些方法都是在基類中實現的,從而會使得每個類的行爲表現得具有一致性。

  要知道的是,上面只是定義了三個接口,只是代表每組MVP的行爲而已。實際上還並沒有實現MVP的搭建,那麼下面,將會通過Base基類搭建一個MVP模式基礎。

Model
public class BaseModel implements Model {
}

  因爲model沒有共有的方法,所以它的基類也是一個空的類。可能從這裏看的話會顯得多餘,畢竟只是一個空的類,那麼就完全沒必要去創建它。而實際上也的確沒必要去創建這個BaseModel,畢竟接口Model本身就是爲了實現model的一致性了。而之所以這裏又重新創建了這個Base類,是因爲後面將會基於該mvp框架搭建RxJava+Retrofit實現的MVP,而引入BaseModel的話,不僅能夠使得兩種MVP的使用具有一致性,同時也可以在BaseModel中添加model可能需要的共同的方法。
  那麼接下來看Presenter的實現。

Presenter
public abstract class BasePresenter<V extends View, M extends Model> implements Presenter {
    protected V mView;
    protected M mModel;

    public BasePresenter(V view) {
        mView = view;
        mModel = createModel();
    }

    @Override
    public void detach() {
        mView = null;
    }

    @Override
    public boolean isAttach() {
        return mView != null;
    }

    @Override
    public void checkAttach() {
        if (!isAttach()){
            throw new RuntimeException("當前Presenter未與View建立連接");
        }
    }

    /**
     * 創建Presenter對應的Model
     *
     * @return Model
     */
    protected abstract M createModel();
}

  從上面可以看到,這裏presenter實現了presenter接口的共有方法,同時還定義了兩個變量,mView 和mModel ,也就是mvp中的另外兩者。並且只提供了一個構造方法,在這個構造方法中完成和View的綁定。這裏就可以知道,我們沒在presenter的接口中定義綁定的方法,是因爲將綁定的過程放到了構造方法,也就是說當Presenter存在的時候就需要綁定View。
  另外,presenter中並沒有直接引用model,而是提供了一個抽象方法來生成model。因爲對於一個presenter而言,model並不是必須存在的,只有當需要獲取數據的時候纔會需要model,因此這裏將model的創建放在了一個抽象方法中,當我們不需要的時候直接返回null即可。

  接下來是View的基類了,因爲view是由Activity和Fragment擔任的,所以這裏將會分成兩個BaseView。

View
public abstract class BaseActivity<P extends Presenter> extends AppCompatActivity implements View {
    protected P mPresenter;
    protected Context mContext;
    private Dialog mProgressDialog;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getContentView());
        mPresenter = createPresenter();
        initView();
        mContext = this;
        mProgressDialog = DialogUtils.getDefaultDialog(mContext);
    }

    @Override
    protected void onDestroy() {
        mPresenter.detach();
        super.onDestroy();
    }

    @Override
    public void showProgress() {
        if (mProgressDialog != null && !mProgressDialog.isShowing()) {
            mProgressDialog.show();
        }
    }

    @Override
    public void hideProgress() {
        if (mProgressDialog.isShowing() && mProgressDialog != null) {
            mProgressDialog.dismiss();
        }
    }

    /**
     * 返回Content佈局id
     *
     * @return 佈局文件id
     */
    protected abstract @LayoutRes int getContentView();

    /**
     * View的初始化工作
     */
    protected abstract void initView();

    /**
     * 創建Presenter
     *
     * @return 與Activity關聯的Presenter
     */
    protected abstract P createPresenter();
}

  上面是使用Activity作爲View而實現的基礎View。在其中的onCreate中創建presenter和進度條,並且在onDestroy中解除了和Presenter的綁定。另外除了實現基本的顯示隱藏進度條外,還抽象出了三個方法。分別是contentView的佈局xml的ID和初始化的方法以及創建Presenter的方法。這樣的話,當具體的View繼承該基類的時候只需要實現這三個方法即可,而不用去考慮其生命週期的變化了。
  此外,爲了對進度條的統一管理,將生成進度條的方法放在了DialogUtils中,這樣當需要修改進度條的樣式的時候將會更加方便。

public class DialogUtils {

    /**
     * 用於生成默認的正在加載的進度條,該進度條用於普通加載時顯示
     *
     * @return 對話框
     */
    public static Dialog getDefaultDialog(Context context) {
        Dialog dialog = new ProgressDialog(context);
        dialog.setCancelable(true);
        dialog.setCanceledOnTouchOutside(false);
        return dialog;
    }
}

  對於Fragment擔任的View與Activity也是一樣,唯一的區別就是他們生命週期的不同,從而在不同的方法中進行Presenter的綁定和解除。

public abstract class BaseFragment<P extends Presenter> extends Fragment implements com.pgaofeng.common.mvp.View {

    protected P mPresenter;
    private Dialog mDialog;
    protected Context mContext;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(getContentView(), container, false);
        initView(view);
        mPresenter = createPresenter();
        mContext = getContext();
        mDialog = DialogUtils.getDefaultDialog(mContext);
        return view;
    }

    @Override
    public void onDestroyView() {
        mPresenter.detach();
        super.onDestroyView();
    }

    @Override
    public void showProgress() {
        if (mDialog != null && !mDialog.isShowing()) {
            mDialog.show();
        }
    }

    @Override
    public void hideProgress() {
        if (mDialog != null && mDialog.isShowing()) {
            mDialog.dismiss();
        }
    }

    /**
     * 獲取content佈局id
     *
     * @return 佈局id
     */
    protected abstract @LayoutRes int getContentView();

    /**
     * 初始化View
     *
     * @param view contentView
     */
    protected abstract void initView(View view);

    /**
     * 創建View對應的Presenter
     *
     * @return Presenter
     */
    protected abstract P createPresenter();

}

  至此,對於基礎MVP的封裝已經完成。也就是說,MVP中的三者已經通過這幾個Base類連接起來了,因此我們的封裝到這裏已經算是完成了。只是完成了MVP的封裝又怎麼能滿足我們的要求呢,接下來當然是要具體的通過實際案例來體驗一下封裝的MVP如何使用了。


案例(使用方式)

  下面將通過一個小的案例來演示如何使用MVP。這裏的案例就是通過點擊一個按鈕實現獲取數據,然後由textView展示。
先看一下xml的佈局。

xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".main.view.MainActivity">
    <TextView
        android:id="@+id/tv_myText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
    <Button
        android:id="@+id/btn_update"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="更新" />
</LinearLayout>

  對於一個功能,MVP的各個部分都應當有相應的具體的行爲,而行爲應當是使用接口來提取的,但是若每組MVP都提出三個接口的話,會使得文件更加的多了。因此,這裏在使用MVP的時候,使用了Contract來合併了每組的mvp接口。

Contract
public interface MainContract {
    interface View {
        /**
         * 更新TextView的text
         *
         * @param text 內容
         */
        void updateText(String text);

        /**
         * 顯示Toast
         *
         * @param message 消息內容
         */
        void showToast(String message);
    }

    interface Presenter {
        /**
         * 更新TextView的內容
         */
        void updateTextViewText();
    }

    interface Model {
        /**
         * 獲取TextView 的內容
         *
         * @param param    請求參數
         * @param callBack 請求回調
         * @param handler  用於更新主線程UI
         */
        void getTextString(String param, ModelCallBack callBack, Handler handler);
    }
}

  從上面可以看到的是,Contract接口組合了每一組MVP的所有接口,這些接口都應當是它們的行爲準則。在Modle中,獲取數據的方法中傳入了一個回調參數,這個參數是在Model獲取到數據後回調的,由於Presenter和Model是單向通信,因此只能使用回調的方式來將結果傳回Presenter。

View
public class MainActivity extends BaseActivity<MainPresenter> implements MainContract.View {

    TextView mTextView;
    Button mButton;

    @Override
    protected int getContentView() {
        return R.layout.activity_main;
    }

    @Override
    protected void initView() {
        mTextView = findViewById(R.id.tv_myText);
        mButton = findViewById(R.id.btn_update);
        mButton.setOnClickListener(v -> {
            mPresenter.updateTextViewText();
        });
    }

    @Override
    protected MainPresenter createPresenter() {
        return new MainPresenter(this,new Handler());
    }

    @Override
    public void updateText(String text) {
        mTextView.setText(text);
    }

    @Override
    public void showToast(String message) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
    }
}

  MainActivity實現了View的接口,同時繼承的是前面封裝的BaseActivity。可以看到的是,Activity已經很簡潔了,它其中的一些方法除了必要的一些外就只剩下操作界面UI的了,這與我們的期望是一致的,View只負責顯示UI即可。
  另外,引入的泛型是MainPresenter,也就是說該View持有的Presenter就是MainPresenter。這樣的話,就可以根據用戶的需求去主動地調用Presenter的方法了,這裏是通過按鈕的點擊來觸發的更新text操作。

Presenter
public class MainPresenter extends BasePresenter<MainActivity, MainModel> implements MainContract.Presenter {
    public MainPresenter(MainActivity view, Handler handler) {
        super(view);
        this.mHandler = handler;
    }

    private String[] params = {"success", "fail"};
    private Handler mHandler;
    private Random random = new Random(System.currentTimeMillis());

    @Override
    protected MainModel createModel() {
        return new MainModel();
    }
    
    @Override
    public void updateTextViewText() {
        mView.showProgress();
        // 模擬獲取數據的幾種狀態,這裏通過參數來決定是否獲取數據成功
        int index = random.nextInt(2);

        mModel.getTextString(params[index], new ModelCallBack() {
            @Override
            public void success(BaseData<?> baseData) {
                checkAttach();
                String s = (String) baseData.getData();
                if (!TextUtils.isEmpty(s)) {
                    mView.updateText(s);
                }
                mView.hideProgress();
                mView.showToast(baseData.getMessage());
            }

            @Override
            public void fail(Throwable throwable) {

                checkAttach();
                mView.showToast("獲取數據失敗!");
                mView.hideProgress();
            }
        }, mHandler);
    }
}

  而MainPresenter中只實現了一個方法,也就是Contract中的presenter中定義的方法。這裏模擬的是獲取數據,但是由於我們並沒有真實的數據可以獲取,這裏就使用隨機來模擬了兩種情況,成功和失敗這兩種情況。
  同時在構造方法中傳入了一個Handler,之所以使用handler,是因爲通常獲取數據是一個耗時的操作,因此應該在子線程中進行。而界面的操作則必須在UI線程中進行,因此這裏使用Handler來操作UI。而從上面可以看出在Presenter中還是調用了Model的獲取數據的,其中由於獲取數據可能是個耗時,因此Model的方法參數傳入的除了參數外還有一個回調和Handler,然後在Model獲取完數據後將會調用這個方法將結果傳回給Presenter。
  而之所以把Handler也傳遞給了Model,是因爲這裏分配職責的時候,把線程的切換交給了Model來處理。也就是說,Model如何獲取數據以及在什麼線程獲取都是由它自己決定,而Presenter只需要它在UI線程中將結果回調回來即可。

CallBack
public interface ModelCallBack {
    /**
     * 請求成功的回調
     *
     * @param baseData 返回值的基礎bean
     */
    void success(BaseData<?> baseData);

    /**
     * 請求失敗的回調
     *
     * @param throwable 錯誤異常信息
     */
    void fail(Throwable throwable);
}

  這裏定義回調接口的時候,已經限定了獲取成功的回調參數。也就是說,Model必須要將數據轉換成我們所需的格式再回調回來。

BaseData
public class BaseData<T> {
    /**
     * 返回碼
     */
    private int code;
    /**
     * 返回提示消息
     */
    private String message;
    /**
     * 返回的實際數據內容
     */
    private T data;
	...
	// getter,setter  
}

  這裏可以看到,BaseData封裝了三個參數,code,Message和Data。其中code和message是對於此次獲取的數據的一個描述,其中code應當是與後臺協議後的返回碼,而message則是對於code的一些描述消息。data使用的是泛型,是真正存儲數據的地方。
  那麼繼續看Model。

Model
public class MainModel extends BaseModel implements MainContract.Model {
    @Override
    public void getTextString(final String param, final ModelCallBack callBack, Handler handler) {
        if (callBack == null) {
            throw new RuntimeException("回調不應爲空");
        }
        new Thread(() -> {
            try {
                // 模擬在子線程中獲取數據
                Thread.sleep(2000);
                // 獲取數據結束結束後切換主線程回調
                handler.post(() -> {
                    BaseData<String> baseData = new BaseData<>();
                    switch (param) {
                        case "success":
                            baseData.setCode(0);
                            baseData.setMessage("獲取成功");
                            baseData.setData("我是獲取到的消息內容");
                            callBack.success(baseData);
                            break;
                        case "fail":
                            callBack.fail(new RuntimeException("人爲異常!"));
                            break;
                        default:
                            break;
                    }
                });

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

    }
}

  Model只有一個方法,就是獲取數據的那個方法。其中開闢了一個子線程,然後在線程中sleep了2秒作爲獲取數據的耗時。然後通過handler在UI線程把數據傳遞給了Presenter,最後Presenter會通知View去展示這個數據。

總結

  MVP模式主要的作用就是解耦,完全是爲了降低耦合度發展而來的一種編碼模式。將界面的展示和數據的獲取進行分離,會使得整個項目的結構更加清晰。這樣不僅能在需求修改的時候及時定位到目標,還能更加方便得進行功能模塊測試。而這樣的分包分模塊更是方便團隊協作,以及新人對項目的接手難度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章