android-MVP架構(gòu)

MVP

簡(jiǎn)介

MVP是模型(Model)、視圖(View)、主持人(Presenter)的縮寫(xiě),分別代表項(xiàng)目中3個(gè)不同的模塊。如圖所示:


image
  • View 對(duì)應(yīng)于Activity、Fragment,負(fù)責(zé)界面的繪制以及與用戶交互
  • Model 依然是業(yè)務(wù)邏輯和實(shí)體模型
  • Presenter 負(fù)責(zé)完成View于Model間的交互

設(shè)計(jì)前思考:

  • 首先在我們常用的MVC模式中,Activity承載了太多,做了不只是視圖層的事情,而程序開(kāi)發(fā)中最重要的 Context 一般也是在視圖層才擁有的,所以我們需要把Context保持在視圖中。
  • MVP相對(duì)于MVC,MVP中是依賴Presenter這個(gè)接口任務(wù)調(diào)度器來(lái)實(shí)現(xiàn)任務(wù)調(diào)度,則視圖層中所有需要進(jìn)行數(shù)據(jù)交互的,都需要將數(shù)據(jù)交給Presenter,而Presenter將調(diào)用Model來(lái)加載數(shù)據(jù)。
  • 在傳統(tǒng)的MVC中,我常用 initView()、initData()、initEvent()、doOther() 這幾個(gè)方法來(lái)實(shí)現(xiàn)數(shù)據(jù)流程加載、界面交互實(shí)現(xiàn)。現(xiàn)在我們需要拆分出來(lái),Activity從BaseActivity中實(shí)現(xiàn)。

經(jīng)過(guò)這樣的構(gòu)思,我們可以先實(shí)踐一下,我們讓View來(lái)實(shí)現(xiàn)Model的接口,View來(lái)調(diào)用presenter,presenter利用面向接口編程的思想來(lái)調(diào)用接口實(shí)現(xiàn)對(duì)View的操作。實(shí)例如下:


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;  //當(dāng)前Activity的標(biāo)記

    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,提供主持人,擁有主持人后才能提交界面數(shù)據(jù)給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為空的時(shí)候,我們需要重新初始化presenter
        if (mPresenter == null) {
            mPresenter = initPresenter();
        }
    }

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

    @Override
    public void onBackPressed() {   //返回按鈕點(diǎn)擊事件
        //當(dāng)Activity中的 進(jìn)度對(duì)話框正在旋轉(zhuǎn)的時(shí)候(數(shù)據(jù)正在加載,網(wǎng)絡(luò)延遲高,數(shù)據(jù)難以加載),關(guān)閉 進(jìn)度對(duì)話框 , 然后可以手動(dòng)執(zhí)行重新加載

        super.onBackPressed();
    }

    /**
     * 恢復(fù)界面后,我們需要判斷我們的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已經(jīng)設(shè)定好了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;

    /**
     * 在子類的構(gòu)造函數(shù)中,設(shè)定參數(shù)為model,這時(shí)候可以presenter調(diào)用接口來(lái)實(shí)現(xiàn)對(duì)界面的操作。
     */
    public BasePresenter(D model) {
        this.model = model;
    }

    public abstract void initData();


}

關(guān)于我這個(gè)Presenter的設(shè)計(jì),我想說(shuō)的是我們需要將各層解耦,那么我的presenter就不應(yīng)該持有Android程序流轉(zhuǎn)的必然因子,如Context、Bundle、Intent、View等,如果我們需要實(shí)現(xiàn)對(duì)界面的操作,必須通過(guò)調(diào)用我們?cè)O(shè)定好的Model來(lái)實(shí)現(xiàn),關(guān)于BaseModel更加簡(jiǎn)單了,直接是一個(gè)空的接口文件,如下:


public interface BaseViewController {
    //這里面添加實(shí)現(xiàn)類需要實(shí)現(xiàn)的方法即可
}

設(shè)計(jì)后的思考

  • presenter作為主持人,應(yīng)該隨著視圖的關(guān)閉而關(guān)閉,所以我們需要在Activity和Fragment的關(guān)閉的時(shí)候,注銷相應(yīng)的presenter
  • 在應(yīng)用程序被銷毀的時(shí)候,我們重啟了程序,但是這時(shí)應(yīng)用的狀態(tài)如果不恢復(fù)到前面的狀態(tài)那么我們需要把對(duì)應(yīng)的presenter重建
  • 在應(yīng)用恢復(fù)后,如果想保持剛才的狀態(tài),那么我們需要在被銷毀前把視圖的狀態(tài)保存,并且恢復(fù)對(duì)應(yīng)的狀態(tài)

說(shuō)了這么多,我們直接手底下見(jiàn)真章:


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;  //權(quán)限申請(qǐng)對(duì)話框
    private MyProgressDialog myProgressDialog;  //進(jìn)度對(duì)話框

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

    @Override
    protected void initEvent() {
        mBt_login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MyApplication.getInstance().outLog(TAG, "MDZZ");    //日志輸出
                //調(diào)用Presenter的登錄的網(wǎng)絡(luò)請(qǐng)求,將用戶名和密碼傳遞過(guò)去
                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("手機(jī)存儲(chǔ)空間");
            if (!addPermission(permissionsList, Manifest.permission.READ_PHONE_STATE))
                permissionsNeeded.add("獲取手機(jī)狀態(tài)");
            if (!addPermission(permissionsList, Manifest.permission.CAMERA))
                permissionsNeeded.add("手機(jī)相機(jī)");
            if (!addPermission(permissionsList, Manifest.permission.ACCESS_COARSE_LOCATION))
                permissionsNeeded.add("手機(jī)位置");
//            if (!addPermission(permissionsList, Manifest.permission.WRITE_SETTINGS))
//                permissionsNeeded.add("手機(jī)設(shè)置");

            if (permissionsList.size() > 0) {
                if (permissionsNeeded.size() > 0) { //待申請(qǐng)的權(quán)限列表
                    // 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) {
                    //經(jīng)過(guò)用戶授權(quán),獲得所有權(quán)限
                    if (appPermission != null) {
                        appPermission = null;
                    }
                    // All Permissions Granted
                } else {    //未得到用戶授權(quán)
                    // Permission Denied
                    appPermission = new AppUpdateDialog(AppUpdateDialog.IMPORTANT, "一些權(quán)限未被允許,請(qǐng)?jiān)谠O(shè)置中授權(quán)!", 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();
    }
}

其實(shí)上面我們當(dāng)中可以看到我們前臺(tái)界面拿到用戶數(shù)據(jù)后,調(diào)用presenter的doLogin方法,把用戶名和密碼傳遞過(guò)去,然后我們?cè)赑resenter中請(qǐng)求網(wǎng)絡(luò)然后再通過(guò)調(diào)用接口實(shí)現(xiàn)數(shù)據(jù)回傳。如下:

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> {
    /**
     * 在子類的構(gòu)造函數(shù)中,設(shè)定參數(shù)為model,這時(shí)候可以presenter調(diào)用接口來(lái)實(shí)現(xiàn)對(duì)界面的操作。
     *
     * @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("用戶名或密碼不能為空!"); //調(diào)用model的錯(cuò)誤提示對(duì)話框
            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);
                //數(shù)據(jù)不為空再進(jìn)行數(shù)據(jù)處理
                try {
                    if (null != t) {
                        MyApplication.getInstance().outLog("輸出", t);
                        LoginBean bean = new GsonUtils().toBean(t, LoginBean.class);
                        if (null != bean) {
                            if (bean.getCode() == 0) {
                                //請(qǐng)求成功
                                model.openHome(bean);
                            } else if (bean.getCode() == 1) {
                                model.showDailog("登錄失敗,帳戶不存在");
                            } else if (bean.getCode() == 2) {
                                model.showDailog("登錄失敗,密碼錯(cuò)誤");
                            } else {
                                model.showDailog("登錄失敗,其他未知錯(cuò)誤");
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    model.showDailog("登錄失敗,其他未知錯(cuò)誤");
                }


            }

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

            @Override
            public void onFinish() {
                super.onFinish();
                model.disProgressD();   //model的關(guān)閉對(duì)話框的接口
            }

            @Override
            public void onPreStart() {
                super.onPreStart();
                model.showProgressD();  //model的進(jìn)度對(duì)話框
            }
        });


    }
}

我們上面可以看到我們現(xiàn)在只要把請(qǐng)求網(wǎng)絡(luò)的數(shù)據(jù)傳遞上去就可以完成單元測(cè)試了,這樣子我們就達(dá)到了我們數(shù)據(jù)流轉(zhuǎn)的單元測(cè)試的標(biāo)準(zhǔn)。

既然我們都看到了Presenter對(duì)model的調(diào)用,那么我們直接貼上model再對(duì)比Activity就能明白了我們是怎么完成這個(gè)設(shè)計(jì)的。

public interface LoginActivityViewController extends BaseViewController {
    /**
     * 顯示信息提示對(duì)話框
     * @param msg   message
     */
    void showDailog(String msg);

    /**
     * 顯示進(jìn)度對(duì)話框
     */
    void showProgressD();

    /**
     * 關(guān)閉對(duì)話框
     */
    void disProgressD();

    /**
     * 登陸成功跳轉(zhuǎn)到其他界面
     * @param bean
     */
    void openHome(LoginBean bean);
}

我們看到這里,很多哥們可能又會(huì)不明白,為什么我們能控制界面呢?如下:

//我們?cè)诔绦蛑?,presenter直接調(diào)用的model,但是model是被View實(shí)現(xiàn)了的。
public class LoginActivity extends BaseActivity<LoginActivityPresenter, LoginActivityViewController> implements LoginActivityViewController {
    @Override
    public void showDailog(String msg) {
        //實(shí)現(xiàn)了model的顯示對(duì)話框的方法
        new MustDoThingDailog("提示", msg, getContext(), new MustDoThingDailog.NeedDoThing() {
            @Override
            public void mustDoThings() {

            }
        });
    }

    @Override
    public void showProgressD() {
        //這是顯示進(jìn)度對(duì)話框的,實(shí)現(xiàn)了model的方法
    }

    @Override
    public void disProgressD() {
        //這是實(shí)現(xiàn)了moel的關(guān)閉進(jìn)度對(duì)話框的方法
    }

    @Override
    public void openHome(LoginBean bean) {

        //實(shí)現(xiàn)了model的打開(kāi)其他頁(yè)面的方法
    }
}

所以我們的MVP執(zhí)行的步驟其實(shí)就是:用戶執(zhí)行操作 -> 調(diào)用presenter(完成獨(dú)立的數(shù)據(jù)處理) -> 調(diào)用model的方法控制界面 -> 展示給用戶


然后應(yīng)該又有哥們會(huì)問(wèn)我,為什么你的基類中會(huì)有<>這種括號(hào)括起來(lái)的東西,恩恩這個(gè)是泛型,主要是用來(lái)說(shuō)明他們是哪一類的東西,通過(guò)泛型來(lái)解耦就可以在基類中整合更多的東西。具體的要我來(lái)說(shuō)明的話,我只能說(shuō)“就不?。?!”,我需要任性一回。關(guān)于MVP更好的介紹可以看下github的項(xiàng)目TheMvp,這個(gè)是我的偶像@張濤寫(xiě)的喲。

總結(jié)

  • 在mvp架構(gòu)中,我們需要在基類中拿到每個(gè)界面對(duì)應(yīng)的presenter和model,則我們需要讓程序知道每個(gè)對(duì)應(yīng)的presenter和model.
  • 為了減少不必要的代碼開(kāi)銷,我們需要把每個(gè)activity和Fragment的公共方法抽取出來(lái),寫(xiě)入基類中.
  • 在基類中,我們需要將具體的presenter和model解耦,則需要泛型進(jìn)行類型轉(zhuǎn)換來(lái)解除耦合.
  • 泛型解除耦合后,我們需要在每個(gè)具體的view中來(lái)持有presenter和實(shí)現(xiàn)model層的接口.并且通過(guò)每個(gè)view關(guān)聯(lián)的presenter調(diào)用model的某個(gè)方法來(lái)控制view.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容