美文网首页Android-技术Android TipsMVP项目
Nucleus,一个好用的MVP框架

Nucleus,一个好用的MVP框架

作者: 李晓通 | 来源:发表于2017-08-17 16:30 被阅读318次

    前言

    今天给大家带来的是一个mvp框架,nucleus。这个框架是由国外的一位大神konmik搭建的,对mvp进行了一个封装,那么就先给大家说说MVP模式。

    MVP模式

    MVP是从经典的模式MVC演变而来,不同的是MVP中Model层并不会直接与View层有任何关系,而是通过Presenter来进行交互,至于MVP的好处这里我也不多陈述,网上有很多文章都对MVP的优点进行介绍,下面就先以传统的MVP给大家写一个登录的Demo感受一下。

    MVP结构

    传统MVP登录

    目录结构

    首先给大家看一下目录结构,当然,不同的人有不同的分包习惯,有的以功能模块分包,有的以传统分包,这个根据自己喜好来就行,这里是以Contract模式来分包,使用contract的好处是因为我们有一个插件,叫MVPHelper,一键生成Presenter和Model层的代码。

    LoginContract
    LoginContract结构
    LoginActivity

    首先用LoginActivity实现LoginContract.View并重写里面的方法,然后在onCreate中拿到对应Presenter层的引用,大家可以看到这里没有任何的逻辑判断,只有界面相关的操作。

    package cn.lxt.mvpdemo.view.activity;
    
    import android.app.ProgressDialog;
    import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    import android.view.View;
    import android.widget.Button;
    import android.widget.EditText;
    import android.widget.Toast;
    
    import cn.lxt.mvpdemo.R;
    import cn.lxt.mvpdemo.contract.LoginContract;
    import cn.lxt.mvpdemo.presenter.LoginPresenter;
    
    //首先要实现LoginContract.View
    public class LoginActivity extends AppCompatActivity implements LoginContract.View, View.OnClickListener {
    
        private ProgressDialog mProgressDialog;
        private EditText mEtName;
        private EditText mEtPsw;
        private LoginContract.Presenter mPresenter;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            //拿到Presenter层的引用
            mPresenter = new LoginPresenter(this);
            mEtName = (EditText) findViewById(R.id.et_name);
            mEtPsw = (EditText) findViewById(R.id.et_psw);
            Button button = (Button) findViewById(R.id.btn_login);
            button.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.btn_login:
                    //点击登录
                    String name = mEtName.getText().toString().trim();
                    String psw = mEtPsw.getText().toString().trim();
                    mPresenter.login(name, psw);
                    break;
            }
        }
    
        //登陆成功后会走这里
        @Override
        public void loginSuccess() {
            Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
        }
    
         //登陆失败后会走这里
        @Override
        public void loginFailed(String msg) {
            Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
        }
    
        @Override
        public void showDialog() {
            mProgressDialog = new ProgressDialog(this);
            mProgressDialog.setMessage("登陆中");
            mProgressDialog.show();
        }
    
        @Override
        public void hideDialog() {
            if (mProgressDialog != null && mProgressDialog.isShowing())
                mProgressDialog.dismiss();
        }
    }
    
    LoginPresenter

    用MvpHelper这个插件一键生成该类,同样的重写方法,在构造中拿到对应Model层的引用。

    package cn.lxt.mvpdemo.presenter;
    
    import cn.lxt.mvpdemo.contract.LoginContract;
    import cn.lxt.mvpdemo.model.LoginModel;
    import cn.lxt.mvpdemo.view.activity.LoginActivity;
    
    /**
     * Created by Administrator on 2017/8/17 0017.
     */
    //这个类是自动生成的
    public class LoginPresenter implements LoginContract.Presenter {
        private final LoginActivity loginActivity;
        private final LoginContract.Model mModel;
    
        public LoginPresenter(LoginActivity loginActivity) {
            this.loginActivity = loginActivity;
            mModel = new LoginModel(this);
        }
    
        @Override
        public void login(String name, String psw) {
            //通知view层显示dialog
            loginActivity.showDialog();
            //通知model层调用登录
            mModel.login(name, psw);
        }
    
        @Override
        public void loginSuccess() {
            //登陆成功之后的回调
            loginActivity.hideDialog();
            loginActivity.loginSuccess();        
        }
    
        @Override
        public void loginFailed(String msg) {
            //登陆失败之后的回调
            loginActivity.hideDialog();
            loginActivity.loginFailed(msg);
        }
    }
    
    LoginModel

    这个类也是插件一键生成的,同样的,在构造中拿到Presenter的引用,在model层处理业务逻辑。

    package cn.lxt.mvpdemo.model;
    
    import android.text.TextUtils;
    
    import java.util.concurrent.TimeUnit;
    
    import cn.lxt.mvpdemo.contract.LoginContract;
    import cn.lxt.mvpdemo.presenter.LoginPresenter;
    import io.reactivex.Observable;
    import io.reactivex.android.schedulers.AndroidSchedulers;
    import io.reactivex.annotations.NonNull;
    import io.reactivex.functions.Consumer;
    import io.reactivex.schedulers.Schedulers;
    
    /**
     * Created by Administrator on 2017/8/17 0017.
     */
    
    public class LoginModel implements LoginContract.Model {
        private final LoginPresenter loginPresenter;
    
        public LoginModel(LoginPresenter loginPresenter) {
            this.loginPresenter = loginPresenter;
        }
    
        @Override
        public void login(final String name, final String psw) {
            //这里我们做逻辑处理
            if (TextUtils.isEmpty(name)) {
                loginPresenter.loginFailed("姓名不允许为空");
            } else if (TextUtils.isEmpty(psw)) {
                loginPresenter.loginFailed("密码不允许为空");
            } else {
                //这里模拟登录过程
                Observable.timer(2, TimeUnit.SECONDS)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(new Consumer<Long>() {
                            @Override
                            public void accept(@NonNull Long aLong) throws Exception {
                                loginPresenter.loginSuccess();
                            }
                        });
            }
        }
    }
    
    演示
    登录演示

    效果很明显,view层没有任何的逻辑处理,有的只是页面的显示,presenter层只是负责view层和model层的交互,model层只是负责逻辑的处理,分工明确,当然,这只是一个最基础的MVP模式,还有很多情况没有考虑到,那么接下来给大家带来一套成熟的MVP框架,也就上面说的nucleus。

    Nucleus

    Nucleus 是一个实现MVP+Rxjava的框架,首先给大家说一下他的好处

    1. 它支持在View/Fragment/Activity的Bundle中保存/恢复Presenter的状态,一个Presenter可以保存它的请求参数到bundles中,以便之后重启它们
    2. 它允许一个View实例持有多个Presenter对象
    3. 快速实现View和Presenter的绑定
    4. 提供线程的基类以便复用
    5. 支持在进程重启后,自动重新发起请求,在onDestroy方法中,自动退订RxJava订阅
    6. 使用相当简单

    那么我们首先来看看他的整体目录结构

    nucleus结构

    整体逻辑都在第一个,下面两个是对v7和v4的扩展。

    nucleus整体结构

    nucleus使用(基于rxjava,不会rxjava的小伙伴请看我之前的文章)

    第一步,添加依赖
        compile 'info.android15.nucleus5:nucleus:5.0.0-beta1'
        compile 'info.android15.nucleus5:nucleus-support-v4:5.0.0-beta1'
        compile 'info.android15.nucleus5:nucleus-support-v7:5.0.0-beta1'
    
        compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
        compile 'io.reactivex.rxjava2:rxjava:2.1.0'
        compile 'com.squareup.retrofit2:adapter-rxjava2:2.2.0'
    
    第二步,继承

    用你项目中的BaseActivity去继承他的NucleusAppCompatActivity,比如

    继承关系

    这样你所有继承BaseActivity的Activity就都可以使用了。

    用你项目中的BaseFragment去继承他的NucleusSupportFragment

    继承关系

    用你的BasePresenter继承他的RxPresenter

    继承关系

    到此为止准备工作已经做完了。

    第三步,网络请求

    还是以刚才的登录为例子,不同的是这里用到了Retrofit+RxJava的模式做的联网请求,我这里用的是真实的网络请求,不是模拟的了。

    首先,在LoginActivity中增加一个注解,里面的参数传入对应的Presenter类,直接上代码。

    package cn.lxt.nucleusdemo.view.activity;
    
    import android.view.View;
    import android.widget.Button;
    import android.widget.EditText;
    import android.widget.Toast;
    
    import cn.lxt.nucleusdemo.R;
    import cn.lxt.nucleusdemo.base.BaseActivity;
    import cn.lxt.nucleusdemo.presenter.LoginPresenter;
    import nucleus5.factory.RequiresPresenter;
    
    //添加一个注解,参数为这个类所对应的Presenter
    @RequiresPresenter(LoginPresenter.class)
    public class LoginActivity extends BaseActivity<LoginPresenter> implements View.OnClickListener {
    
        private EditText mEtName;
        private EditText mEtPsw;
        private Button mButton;
    
        @Override
        protected void initLayout() {
            setContentView(R.layout.activity_main);
        }
    
        @Override
        protected void initView() {
            mEtName = (EditText) findViewById(R.id.et_name);
            mEtPsw = (EditText) findViewById(R.id.et_psw);
            mButton = (Button) findViewById(R.id.btn_login);
        }
    
        @Override
        protected void initData() {
        }
    
        @Override
        protected void initClickListener() {
            mButton.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.btn_login:
                    String name = mEtName.getText().toString().trim();
                    String psw = mEtPsw.getText().toString().trim();
                    //注意,当你点击登录时,可以调用getPresenter()拿到对应P层的引用
                    getPresenter().login(this, name, psw);
                    break;
            }
        }
    
        public void loginSuccess() {
            Toast.makeText(this, "登陆成功", Toast.LENGTH_SHORT).show();
        }
    
        public void loginFailed() {
            Toast.makeText(this, "登陆失败", Toast.LENGTH_SHORT).show();
        }
    }
    
    重头戏来了,Presenter里面的逻辑代码

    还是同样的,继承你自己的BasePresenter,不同的是你可以使用nucleus帮你封装的生命周期方法了,重写onCreate方法。
    在你需要调用请求的地方,调用start()方法,参数为一个int值,你可以自己定义。

    package cn.lxt.nucleusdemo.presenter;
    
    import android.content.Context;
    import android.os.Bundle;
    import android.text.TextUtils;
    import android.widget.Toast;
    
    import cn.lxt.nucleusdemo.api.Service;
    import cn.lxt.nucleusdemo.base.BasePresenter;
    import cn.lxt.nucleusdemo.response.LoginResponse;
    import cn.lxt.nucleusdemo.retrofit.RetrofitUtil;
    import cn.lxt.nucleusdemo.view.activity.LoginActivity;
    import io.reactivex.Observable;
    import io.reactivex.android.schedulers.AndroidSchedulers;
    import io.reactivex.annotations.NonNull;
    import io.reactivex.disposables.Disposable;
    import io.reactivex.functions.BiConsumer;
    import io.reactivex.functions.Consumer;
    import io.reactivex.schedulers.Schedulers;
    import nucleus5.presenter.Factory;
    
    /**
     * Created by Administrator on 2017/8/17 0017.
     */
    
    public class LoginPresenter extends BasePresenter<LoginActivity> {
    
        private String name, psw;
        private final int REQUSET_LOGIN = 0;
    
        @Override
        protected void onCreate(Bundle savedState) {
            super.onCreate(savedState);
            //调用该方法开始一个请求,第一个参数就是你start里面传入的int值,第二个参数就是一个Factory,所有的联网逻辑都写在里面,这里结合了Retrofit,第三个参数就是请求成功的回调,第四个参数就是请求失败的回调
            restartableLatestCache(REQUSET_LOGIN, new Factory<Observable<LoginResponse>>() {
                @Override
                public Observable<LoginResponse> create() {
                    return RetrofitUtil.getRetrofit(context)
                            .create(Service.class)
                            .login(name, psw, "APP")
                            .subscribeOn(Schedulers.io())
                            .doOnSubscribe(new Consumer<Disposable>() {
                                @Override
                                public void accept(@NonNull Disposable disposable) throws Exception {
                                    ((LoginActivity) context).showDialog();
                                }
                            })
                            .observeOn(AndroidSchedulers.mainThread());
                }
            }, new BiConsumer<LoginActivity, LoginResponse>() {
                @Override
                public void accept(LoginActivity loginActivity, LoginResponse loginResponse) throws Exception {
                    loginActivity.loginSuccess();
                    loginActivity.hideDialog();
                    //请求成功之后调用stop(),参数为start里面传入的参数
                    stop(REQUSET_LOGIN);
                }
            }, new BiConsumer<LoginActivity, Throwable>() {
                @Override
                public void accept(LoginActivity loginActivity, Throwable throwable) throws Exception {
                    loginActivity.loginFailed();
                    loginActivity.hideDialog();
                    //请求失败之后调用stop(),参数为start里面传入的参数
                    stop(REQUSET_LOGIN);
                }
            });
        }
    
        public void login(Context context, String name, String psw) {
            if (TextUtils.isEmpty(name)) {
                Toast.makeText(context, "用户名不能为空", Toast.LENGTH_SHORT).show();
            } else if (TextUtils.isEmpty(psw)) {
                Toast.makeText(context, "密码不能为空", Toast.LENGTH_SHORT).show();
            } else {
                this.context = context;
                this.name = name;
                this.psw = psw;
                start(REQUSET_LOGIN);
            }
        }
    }
    
    演示

    这里不是模拟的登录了,而是真实的登录


    Nucleus登录成功

    Nucleus源码浅析

    到了这里,可能很多人就说,我做这么多道理有什么用呢?别着急,接下来带大家看看他的源码。

    首先,我们看看继承他的NucleusAppCompatActivity做了什么事

    在这个类中有一句这样的代码,拿到了我们定义的presenter层对象


    然后把这个presenter对象与我们的activity的生命周期进行绑定,大家可以看到,在onSaveInstanceState里面,他帮我们保存了数据,这也是上面说的它支持在View/Fragment/Activity的Bundle中保存/恢复Presenter的状态,一个Presenter可以保存它的请求参数到bundles中,以便之后重启它们

    再看RxPresenter又做了一些什么事
    首先我们找到了start方法,首先不管有没有开启,nucleus先帮我们停止了任务,然后在把我们的id添加到了requested这个集合中,然后把id和我们即将开启的Factory放到了hashmap中进行对应,也就是第二个参数中的Factory,这也是为什么我们能通过stop(id)或者start(id)来控制的原因

    start方法

    接下来看看我们在onCreate中调用的restartableLatestCache方法

    restartable方法就是把id和factory进行绑定,然后开启了rxjava并将onNext和onError回调给我们,也就是我们传入的第三个和第四个参数。

    总结

    今天的Nucleus框架使用就给大家讲到这里,如果本文中有任何错误欢迎指出,同时也欢迎喜欢这个框架的朋友一起讨论,我们一起学习一起进步。

    以上纯属于个人平时工作和学习的一些总结分享,如果有什么错误欢迎随时指出,大家可以讨论一起进步。

    相关文章

      网友评论

      本文标题:Nucleus,一个好用的MVP框架

      本文链接:https://www.haomeiwen.com/subject/ioogrxtx.html