Android-MVP架構

pc859107393發表於2019-03-04

MVP

簡介

MVP是模型(Model)、檢視(View)、主持人(Presenter)的縮寫,分別代表專案中3個不同的模組。如圖所示:

Android-MVP架構
image
  • View 對應於Activity、Fragment,負責介面的繪製以及與使用者互動
  • Model 依然是業務邏輯和實體模型
  • Presenter 負責完成View於Model間的互動

    設計前思考:

  • 首先在我們常用的MVC模式中,Activity承載了太多,做了不只是檢視層的事情,而程式開發中最重要的 Context 一般也是在檢視層才擁有的,所以我們需要把Context保持在檢視中。
  • MVP相對於MVC,MVP中是依賴Presenter這個介面任務排程器來實現任務排程,則檢視層中所有需要進行資料互動的,都需要將資料交給Presenter,而Presenter將呼叫Model來載入資料。
  • 在傳統的MVC中,我常用 initView()、initData()、initEvent()、doOther() 這幾個方法來實現資料流程載入、介面互動實現。現在我們需要拆分出來,Activity從BaseActivity中實現。

經過這樣的構思,我們可以先實踐一下,我們讓View來實現Model的介面,View來呼叫presenter,presenter利用面向介面程式設計的思想來呼叫介面實現對View的操作。例項如下:


import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

import com.acheng.achengutils.mvp.model.BaseViewController;
import com.acheng.achengutils.mvp.presenter.BasePresenter;


/**
 * Created by pc859107393 on 2016/6/28.
 */
public abstract class BaseActivity<T extends BasePresenter, M extends BaseViewController> extends AppCompatActivity {

    public String TAG;  //當前Activity的標記

    protected T mPresenter;     //主持人角色

    protected abstract T initPresenter();    //獲取到主持人


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TAG = String.format("%s::%s", getPackageName(), getLocalClassName());


        mPresenter = initPresenter();    //初始化Presenter,提供主持人,擁有主持人後才能提交介面資料給presenter

        setContentView(setLayoutId());

        initView();

        mPresenter.initData();

        initEvent();

        doOther();
    }

    protected void doOther() {

    }

    public Context getContext() {
        return this;
    }

    protected abstract void initEvent();


    protected abstract void initView();

    protected abstract int setLayoutId();

    @Override
    protected void onResume() {
        super.onResume();
        //如果presenter為空的時候,我們需要重新初始化presenter
        if (mPresenter == null) {
            mPresenter = initPresenter();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
    }

    @Override
    public void onBackPressed() {   //返回按鈕點選事件
        //當Activity中的 進度對話方塊正在旋轉的時候(資料正在載入,網路延遲高,資料難以載入),關閉 進度對話方塊 , 然後可以手動執行重新載入

        super.onBackPressed();
    }

    /**
     * 恢復介面後,我們需要判斷我們的presenter是不是存在,不存在則重置presenter
     *
     * @param savedInstanceState
     */
    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        if (mPresenter == null)
            mPresenter = initPresenter();
    }

    /**
     * onDestroy中銷燬presenter
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPresenter = null;
    }

}複製程式碼

既然我們的Activity已經設定好了BaseActivity,我們需要接著完成BasePresenter,如下:

import com.acheng.achengutils.mvp.model.BaseViewController;

/**
 * Created by acheng on 2016/7/14.
 */
public abstract class BasePresenter<D extends BaseViewController> {


    public D model;

    /**
     * 在子類的建構函式中,設定引數為model,這時候可以presenter呼叫介面來實現對介面的操作。
     */
    public BasePresenter(D model) {
        this.model = model;
    }

    public abstract void initData();


}複製程式碼

關於我這個Presenter的設計,我想說的是我們需要將各層解耦,那麼我的presenter就不應該持有Android程式流轉的必然因子,如Context、Bundle、Intent、View等,如果我們需要實現對介面的操作,必須通過呼叫我們設定好的Model來實現,關於BaseModel更加簡單了,直接是一個空的介面檔案,如下:


public interface BaseViewController {
    //這裡面新增實現類需要實現的方法即可
}複製程式碼

設計後的思考

  • presenter作為主持人,應該隨著檢視的關閉而關閉,所以我們需要在Activity和Fragment的關閉的時候,登出相應的presenter
  • 在應用程式被銷燬的時候,我們重啟了程式,但是這時應用的狀態如果不恢復到前面的狀態那麼我們需要把對應的presenter重建
  • 在應用恢復後,如果想保持剛才的狀態,那麼我們需要在被銷燬前把檢視的狀態儲存,並且恢復對應的狀態

說了這麼多,我們直接手底下見真章:


import android.Manifest;
import android.annotation.TargetApi;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.support.v7.app.AlertDialog;
import android.view.View;
import android.widget.TextView;

import com.acheng.achengutils.mvp.view.BaseActivity;
import com.acheng.achengutils.utils.SPHelper;
import com.acheng.achengutils.widgets.AppUpdateDialog;
import com.acheng.achengutils.widgets.MustDoThingDailog;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import acheng1314.cn.a3dbuild.MyApplication;
import acheng1314.cn.a3dbuild.R;
import acheng1314.cn.a3dbuild.bean.LoginBean;
import acheng1314.cn.a3dbuild.view.activity.presenter.LoginActivityPresenter;
import acheng1314.cn.a3dbuild.view.activity.viewcontroller.LoginActivityViewController;
import acheng1314.cn.a3dbuild.widgets.MyProgressDialog;

/**
 * Created by pc859107393 on 2016/9/12 0012.
 */
public class LoginActivity extends BaseActivity<LoginActivityPresenter, LoginActivityViewController> implements LoginActivityViewController {

    private View mBt_login;
    private TextView mEt_username;  //使用者名稱
    private TextView mEt_password;  //密碼s


    final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;
    private AppUpdateDialog appPermission;  //許可權申請對話方塊
    private MyProgressDialog myProgressDialog;  //進度對話方塊

    @Override
    protected LoginActivityPresenter initPresenter() {
        return new LoginActivityPresenter(this);    //例項化LoginActivity的Presenter
    }

    @Override
    protected void initEvent() {
        mBt_login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MyApplication.getInstance().outLog(TAG, "MDZZ");    //日誌輸出
                //呼叫Presenter的登入的網路請求,將使用者名稱和密碼傳遞過去
                mPresenter.doLogin(mEt_username.getText().toString(), mEt_password.getText().toString()); 



            }
        });
    }

    @Override
    protected void initView() {
        MyApplication.getInstance().addActivity(this);  //將Activity加入堆疊管理
        mEt_username = (TextView) findViewById(R.id.mEt_username);
        mEt_password = (TextView) findViewById(R.id.mEt_password);
        mBt_login = findViewById(R.id.mBt_login);
    }

    @Override
    protected void doOther() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            List<String> permissionsNeeded = new ArrayList<String>();

            final List<String> permissionsList = new ArrayList<String>();
            if (!addPermission(permissionsList, Manifest.permission.WRITE_EXTERNAL_STORAGE))
                permissionsNeeded.add("手機儲存空間");
            if (!addPermission(permissionsList, Manifest.permission.READ_PHONE_STATE))
                permissionsNeeded.add("獲取手機狀態");
            if (!addPermission(permissionsList, Manifest.permission.CAMERA))
                permissionsNeeded.add("手機相機");
            if (!addPermission(permissionsList, Manifest.permission.ACCESS_COARSE_LOCATION))
                permissionsNeeded.add("手機位置");
//            if (!addPermission(permissionsList, Manifest.permission.WRITE_SETTINGS))
//                permissionsNeeded.add("手機設定");

            if (permissionsList.size() > 0) {
                if (permissionsNeeded.size() > 0) { //待申請的許可權列表
                    // Need Rationale
                    String message = "你必須允許本APP使用:" + permissionsNeeded.get(0);
                    for (int i = 1; i < permissionsNeeded.size(); i++)
                        message = message + ", " + permissionsNeeded.get(i);
                    showMessageOKCancel(message,
                            new DialogInterface.OnClickListener() {
                                @TargetApi(Build.VERSION_CODES.M)
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                                            REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
                                }
                            });
                    return;
                }
                requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                        REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
            }
        }
        super.doOther();
    }

    private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
        new AlertDialog.Builder(this)
                .setMessage(message)
                .setPositiveButton("允許", okListener)
                .setNegativeButton("拒絕", null)
                .create()
                .show();
    }

    private boolean addPermission(List<String> permissionsList, String permission) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
                permissionsList.add(permission);
                if (!shouldShowRequestPermissionRationale(permission))
                    return false;
            }
        }
        return true;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS: {
                Map<String, Integer> perms = new HashMap<String, Integer>();
                // Initial
                perms.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, PackageManager.PERMISSION_GRANTED);
                perms.put(Manifest.permission.READ_PHONE_STATE, PackageManager.PERMISSION_GRANTED);
                perms.put(Manifest.permission.CAMERA, PackageManager.PERMISSION_GRANTED);
                perms.put(Manifest.permission.ACCESS_COARSE_LOCATION, PackageManager.PERMISSION_GRANTED);
                // Fill with results
                for (int i = 0; i < permissions.length; i++)
                    perms.put(permissions[i], grantResults[i]);
                // Check for ACCESS_FINE_LOCATION
                if (perms.get(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
                        && perms.get(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED
                        && perms.get(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
                        && perms.get(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
                    //經過使用者授權,獲得所有許可權
                    if (appPermission != null) {
                        appPermission = null;
                    }
                    // All Permissions Granted
                } else {    //未得到使用者授權
                    // Permission Denied
                    appPermission = new AppUpdateDialog(AppUpdateDialog.IMPORTANT, "一些許可權未被允許,請在設定中授權!", getContext(), new AppUpdateDialog.NeedDoThing() {
                        @Override
                        public void mustDoThing() {
                            Uri packageURI = Uri.parse("package:" + getPackageName());
                            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI);
                            startActivity(intent);
                        }
                    });
                }
            }
            break;
            default:
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        doOther();
    }

    @Override
    protected int setLayoutId() {
        return R.layout.activity_login;
    }

    @Override
    public void showDailog(String msg) {
        new MustDoThingDailog("提示", msg, getContext(), new MustDoThingDailog.NeedDoThing() {
            @Override
            public void mustDoThings() {

            }
        });
    }

    @Override
    public void showProgressD() {
        if (null == myProgressDialog)
            myProgressDialog = new MyProgressDialog("登陸", "正在登入···", getContext());
        else
            myProgressDialog.show();
    }

    @Override
    public void disProgressD() {
        if (null != myProgressDialog)
            myProgressDialog.dismiss();
    }

    @Override
    public void openHome(LoginBean bean) {

        SPHelper.setString(getContext(), getContext().getString(R.string.user), getContext().getString(R.string.username), mEt_username.getText().toString());
        SPHelper.setString(getContext(), getContext().getString(R.string.user), getContext().getString(R.string.password), mEt_password.getText().toString());
        SPHelper.setString(getContext(), getContext().getString(R.string.user), getContext().getString(R.string.userId), bean.getResult().getUserId());
        SPHelper.setString(getContext(), getContext().getString(R.string.user), getContext().getString(R.string.token), bean.getResult().getToken());

        startActivity(new Intent(getContext(), HomeActivity.class));

        finish();
    }
}複製程式碼

其實上面我們當中可以看到我們前臺介面拿到使用者資料後,呼叫presenter的doLogin方法,把使用者名稱和密碼傳遞過去,然後我們在Presenter中請求網路然後再通過呼叫介面實現資料回傳。如下:

import com.acheng.achengutils.gsonutil.GsonUtils;
import com.acheng.achengutils.mvp.presenter.BasePresenter;
import com.acheng.achengutils.utils.CipherUtils;
import com.acheng.achengutils.utils.StringUtils;
import com.kymjs.rxvolley.RxVolley;
import com.kymjs.rxvolley.client.HttpCallback;
import com.kymjs.rxvolley.client.HttpParams;
import com.kymjs.rxvolley.http.VolleyError;

import acheng1314.cn.a3dbuild.MyApplication;
import acheng1314.cn.a3dbuild.bean.LoginBean;
import acheng1314.cn.a3dbuild.hostApi.MyApi;
import acheng1314.cn.a3dbuild.view.activity.viewcontroller.LoginActivityViewController;

/**
 * Created by pc859107393 on 2016/9/12 0012.
 */
public class LoginActivityPresenter extends BasePresenter<LoginActivityViewController> {
    /**
     * 在子類的建構函式中,設定引數為model,這時候可以presenter呼叫介面來實現對介面的操作。
     *
     * @param model
     */
    public LoginActivityPresenter(LoginActivityViewController model) {
        super(model);
    }

    @Override
    public void initData() {

    }

    public void doLogin(String name, String pwd) {
        //使用者名稱和密碼不能為空
        if (StringUtils.isEmpty(name) || StringUtils.isEmpty(pwd)) {
            model.showDailog("使用者名稱或密碼不能為空!"); //呼叫model的錯誤提示對話方塊
            return;
        }

        //密碼MD5加密
        pwd = CipherUtils.small32md5(pwd);
        HttpParams params = new HttpParams();
        params.put("userName", name);
        params.put("passWord", pwd);
        RxVolley.post(MyApi.LoginApi, params, new HttpCallback() {
            @Override
            public void onSuccess(String t) {
                super.onSuccess(t);
                //資料不為空再進行資料處理
                try {
                    if (null != t) {
                        MyApplication.getInstance().outLog("輸出", t);
                        LoginBean bean = new GsonUtils().toBean(t, LoginBean.class);
                        if (null != bean) {
                            if (bean.getCode() == 0) {
                                //請求成功
                                model.openHome(bean);
                            } else if (bean.getCode() == 1) {
                                model.showDailog("登入失敗,帳戶不存在");
                            } else if (bean.getCode() == 2) {
                                model.showDailog("登入失敗,密碼錯誤");
                            } else {
                                model.showDailog("登入失敗,其他未知錯誤");
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    model.showDailog("登入失敗,其他未知錯誤");
                }


            }

            @Override
            public void onFailure(VolleyError error) {
                super.onFailure(error);
                model.showDailog("登入失敗,其他未知錯誤");
            }

            @Override
            public void onFinish() {
                super.onFinish();
                model.disProgressD();   //model的關閉對話方塊的介面
            }

            @Override
            public void onPreStart() {
                super.onPreStart();
                model.showProgressD();  //model的進度對話方塊
            }
        });


    }
}複製程式碼

我們上面可以看到我們現在只要把請求網路的資料傳遞上去就可以完成單元測試了,這樣子我們就達到了我們資料流轉的單元測試的標準。

既然我們都看到了Presenter對model的呼叫,那麼我們直接貼上model再對比Activity就能明白了我們是怎麼完成這個設計的。

public interface LoginActivityViewController extends BaseViewController {
    /**
     * 顯示資訊提示對話方塊
     * @param msg   message
     */
    void showDailog(String msg);

    /**
     * 顯示進度對話方塊
     */
    void showProgressD();

    /**
     * 關閉對話方塊
     */
    void disProgressD();

    /**
     * 登陸成功跳轉到其他介面
     * @param bean
     */
    void openHome(LoginBean bean);
}複製程式碼

我們看到這裡,很多哥們可能又會不明白,為什麼我們能控制介面呢?如下:

//我們在程式中,presenter直接呼叫的model,但是model是被View實現了的。
public class LoginActivity extends BaseActivity<LoginActivityPresenter, LoginActivityViewController> implements LoginActivityViewController {
    @Override
    public void showDailog(String msg) {
        //實現了model的顯示對話方塊的方法
        new MustDoThingDailog("提示", msg, getContext(), new MustDoThingDailog.NeedDoThing() {
            @Override
            public void mustDoThings() {

            }
        });
    }

    @Override
    public void showProgressD() {
        //這是顯示進度對話方塊的,實現了model的方法
    }

    @Override
    public void disProgressD() {
        //這是實現了moel的關閉進度對話方塊的方法
    }

    @Override
    public void openHome(LoginBean bean) {

        //實現了model的開啟其他頁面的方法
    }
}複製程式碼

所以我們的MVP執行的步驟其實就是:使用者執行操作 -> 呼叫presenter(完成獨立的資料處理) -> 呼叫model的方法控制介面 -> 展示給使用者


然後應該又有哥們會問我,為什麼你的基類中會有<>這種括號括起來的東西,恩恩這個是泛型,主要是用來說明他們是哪一類的東西,通過泛型來解耦就可以在基類中整合更多的東西。具體的要我來說明的話,我只能說“就不!!!”,我需要任性一回。關於MVP更好的介紹可以看下github的專案TheMvp,這個是我的偶像@張濤寫的喲。

總結

  • 在mvp架構中,我們需要在基類中拿到每個介面對應的presenter和model,則我們需要讓程式知道每個對應的presenter和model.
  • 為了減少不必要的程式碼開銷,我們需要把每個activity和Fragment的公共方法抽取出來,寫入基類中.
  • 在基類中,我們需要將具體的presenter和model解耦,則需要泛型進行型別轉換來解除耦合.
  • 泛型解除耦合後,我們需要在每個具體的view中來持有presenter和實現model層的介面.並且通過每個view關聯的presenter呼叫model的某個方法來控制view.

相關文章