MVP设计模式的优化与实践

    APP开发最让人头疼的就是业务逻辑和UI代码混合起来,造成后期APP很难维护,新需求越来越难加入,因为依赖太多了,而且纷繁复杂。后来有人提出MVP模式,既然业务逻辑要依赖UI,那么为什么不把他们之间用一个接口隔离开呢,让业务逻辑依赖UI更新的接口而不管具体的UI是如何更新的,更新UI的具体代码则交给Activity来做。当我们进行实践的时候,问题来了,我们需要定义大量接口,而且看似架构很严谨的MVP模式在实际应用当中变得过于臃肿。好了,既然出现了这个问题,那就得解决,Presenter臃肿,有人单独提出一个Presenter分开成多个并且按功能模块划分,对于Presenter来说这样划分是很合理的。同理ViewModel。 但是IView层就还是老样子,即使我们缩减接口,并用方法参数代替要更新哪个UI也没有解决接口爆炸的问题。而且还有了新的问题,那就是我更新UI的时候Activity我并不知道是否已经走了onDestory。 MVP的设计模式根本问题在于接口爆炸,要想减少接口,那必须让业务逻辑和UI逻辑不依赖同一个接口,那尝试一下让UI依赖Model吧,也就是说不管我们业务逻辑如何变化,最终更新UI我们只需要更新Model,MVVM诞生了。

    我觉得没有必要纠结形式,MVVM其实就是观察者模式,使用观察者代替接口,而被观测的对象就是Model。我们再也不需要在UI需要更新的时候去通知接口了,当需要更新UI的时候直接通知观察者。而观察者保存在Activity当中,并且感知Activity的生命周期变化,这样观察者就可以决定是否要执行UI的更新。

    总结,我们仍然可以使用Presenter来划分我们的业务逻辑,然后当需要更新UI的时候通知观察者,即更新Model来驱动,如此Activity依赖ViewModel注册观察者, Presenter依赖ViewModel通知观察者。我认为,这应该才是MVP最佳的实践。由接口爆炸变成了观察者和被观察者爆炸,但是我们不用严格的遵循必须实现那个定死的更新UI的接口了。我们将每个业务过程当做一个状态机,随着业务流程的进行,model的状态会发生改变,这样我们通过观察者来更新UI就成为了可能, 并且这些状态不是零散的,而是随业务逻辑有规则变化的。

举个简单的demo感受一下新的MVP设计模式,以用户登录为例。

这是我们的Model,我们定义了一个登录状态。

public class MainActivityModel extends ViewModel{
    public static final int LOGIN_START   =  1;
    public static final int LOGIN_SUCCESS =  2;
    public static final int LOGIN_FAIL    = -1;
    public MutableLiveData<Integer> loginState = new MutableLiveData<>();
}

这是我们的Presenter, doLogin模拟了登录请求

public class MainActivityPresenter {

    private MainActivityModel mModel;

    public void registerViewModel(ViewModel model){
        mModel = (MainActivityModel) model;
    }


    public void doLogin(String userName, String userPswd){
        //我们使用一线程来模拟用户登录的网络请求, 这里我们就不处理方法的参数了
        new Thread(new Runnable() {
            @Override
            public void run() {
                mModel.loginState.postValue(MainActivityModel.LOGIN_START);
                try {
                    //与服务器交互耗时2s
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    mModel.loginState.postValue(MainActivityModel.LOGIN_FAIL);
                }
                mModel.loginState.postValue(MainActivityModel.LOGIN_SUCCESS);
            }
        }).start();


    }


}

这是我们的主Activity:完成了拼装,注意看我们是如何处理UI更新的


public class MainActivity extends AppCompatActivity {

    private EditText mUserNameEdit;
    private EditText mUserPswdEdit;
    private Button mLoginBtn;
    private ProgressBar mProgressBar;

    private MainActivityPresenter mPresenter;
    private MainActivityModel mModel;

    private void findView(){
        mUserNameEdit = findViewById(R.id.username_edit);
        mUserPswdEdit = findViewById(R.id.userpswd_edit);
        mLoginBtn = findViewById(R.id.login_btn);
        mProgressBar = findViewById(R.id.progress_bar);
    }

//初始化model 和presenter, 并且注册观察者
    private void initMVP(){
        mPresenter = new MainActivityPresenter();
        mModel = ViewModelProviders.of(this).get(MainActivityModel.class);
        mPresenter.registerViewModel(mModel);
        registerObservers();
    }

    private void registerObservers(){
        //为登录状态注册观察者
        mModel.loginState.observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(@Nullable Integer integer) {
                switch(integer){
                    case MainActivityModel.LOGIN_START:{
                        //开始登录的时候我们显示一个进度条
                        mProgressBar.setVisibility(View.VISIBLE);
                        break;
                    }
                    case MainActivityModel.LOGIN_SUCCESS:{
                        //登录成功我们隐藏进度条,并且提示用户登录成功
                        mProgressBar.setVisibility(View.GONE);
                        Toast.makeText(MainActivity.this, "login success",
                                Toast.LENGTH_SHORT).show();
                        break;
                    }
                    case MainActivityModel.LOGIN_FAIL:{
                        //登录成功我们隐藏进度条,并且提示用户登录失败
                        mProgressBar.setVisibility(View.GONE);
                        Toast.makeText(MainActivity.this, "login fail",
                                Toast.LENGTH_SHORT).show();
                        break;
                    }
                    default:{
                        mProgressBar.setVisibility(View.GONE);
                        Toast.makeText(MainActivity.this, "Unknown login state",
                                Toast.LENGTH_SHORT).show();
                    }
                }
            }
        });
    }


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

        mLoginBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //调用presenter处理业务逻辑
                mPresenter.doLogin(mUserNameEdit.getEditableText().toString(),
                        mUserPswdEdit.getEditableText().toString());
            }
        });
    }


}

看到了吧,我们不需要实现任何接口即可实现业务逻辑和UI代码的分离,UI的更新完全依赖与业务逻辑所处的状态。

你也可以将数据Integer换成别的类型,但是应该包含一个状态用来更新UI。比如这样

public class User {
    public int state;
    public String username;
    public String userpswd;
}
public class MainActivityModel extends ViewModel{
    public static final int LOGIN_START   =  1;
    public static final int LOGIN_SUCCESS =  2;
    public static final int LOGIN_FAIL    = -1;
    public MutableLiveData<User> loginState = new MutableLiveData<>();
}

    private void registerObservers(){
        //为登录状态注册观察者
        mModel.loginState.observe(this, new Observer<User>() {
            @Override
            public void onChanged(@Nullable User user) {
                switch(user.state){
                    case MainActivityModel.LOGIN_START:{
                        //开始登录的时候我们显示一个进度条
                        mProgressBar.setVisibility(View.VISIBLE);
                        break;
                    }
                    case MainActivityModel.LOGIN_SUCCESS:{
                        //登录成功我们隐藏进度条,并且提示用户登录成功
                        mProgressBar.setVisibility(View.GONE);
                        Toast.makeText(MainActivity.this, "login success",
                                Toast.LENGTH_SHORT).show();
                        break;
                    }
                    case MainActivityModel.LOGIN_FAIL:{
                        //登录成功我们隐藏进度条,并且提示用户登录失败
                        mProgressBar.setVisibility(View.GONE);
                        Toast.makeText(MainActivity.this, "login fail",
                                Toast.LENGTH_SHORT).show();
                        break;
                    }
                    default:{
                        mProgressBar.setVisibility(View.GONE);
                        Toast.makeText(MainActivity.this, "Unknown login state",
                                Toast.LENGTH_SHORT).show();
                    }
                }
            }
        });
    }




这种模式有哪些缺点呢?首先问题出在postvalue上,postvalue是异步的,并且postvalue在实现上为了考虑效率,如果连续调用postvalue,那么post到主线程的是最后一次post的值,和我们的预期不符,那怎么办呢????  如果能全部post过去就行了

public class MainActivityPresenter {

    private MainActivityModel mModel;
    private Handler mHandler = new Handler(Looper.getMainLooper());

    public void registerViewModel(ViewModel model){
        mModel = (MainActivityModel) model;
    }


    public void doLogin(final String userName, final String userPswd){
        //我们使用一线程来模拟用户登录的网络请求, 这里我们就不处理方法的参数了


        new Thread(new Runnable() {
            @Override
            public void run() {
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        //mModel.loginState.postValue(new Integer(MainActivityModel.LOGIN_START));
                        mModel.loginState.setValue(MainActivityModel.LOGIN_START);
                    }
                });
                try {
                    //与服务器交互耗时2s
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            //mModel.loginState.postValue(new Integer(MainActivityModel.LOGIN_FAIL));
                            mModel.loginState.setValue(MainActivityModel.LOGIN_FAIL);
                        }
                    });

                }


                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        //mModel.loginState.postValue(new Integer(MainActivityModel.LOGIN_SUCCESS));
                        mModel.loginState.setValue(MainActivityModel.LOGIN_SUCCESS);
                    }
                });

                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        //mModel.loginState.postValue(new Integer(MainActivityModel.LOGIN_FINISH));
                        mModel.loginState.setValue(MainActivityModel.LOGIN_FINISH);
                    }
                });

                User user = new User();
                user.username = userName;
                user.userpswd = userPswd;
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        //mModel.user.postValue(user);
                        mModel.user.setValue(user);
                    }
                });


            }
        }).start();


    }


}

但是这样做代码太不优雅了,好多runnable,如果你用lamda代替也是很不美观,继续优化封装,我们自定义一个LiveData

public class SequencePostLiveData<T> extends MutableLiveData<T> {
    
    private Handler mHandler = new Handler(Looper.getMainLooper());
    /**
     * sequencially update value
     * @param value
     */
    public void seqPostValue(T value){

        mHandler.post(new Runnable() {
            @Override
            public void run() {
                setValue(value);
            }
        });
    }
}

然后代码这样改写:

public class MainActivityModel extends ViewModel{
    public static final int LOGIN_START   =  1;
    public static final int LOGIN_SUCCESS =  2;
    public static final int LOGIN_FINISH  =  3;
    public static final int LOGIN_FAIL    = -1;
    public SequencePostLiveData<Integer> loginState = new SequencePostLiveData<>();
    public SequencePostLiveData<User> user = new SequencePostLiveData<>();
}
public class MainActivityPresenter {

    private MainActivityModel mModel;
    public void registerViewModel(ViewModel model){
        mModel = (MainActivityModel) model;
    }


    public void doLogin(final String userName, final String userPswd){
        //我们使用一线程来模拟用户登录的网络请求, 这里我们就不处理方法的参数了


        new Thread(new Runnable() {
            @Override
            public void run() {
                mModel.loginState.seqPostValue(MainActivityModel.LOGIN_START);
                try {
                    //与服务器交互耗时2s
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    mModel.loginState.seqPostValue(MainActivityModel.LOGIN_FAIL);
                }
                
                mModel.loginState.seqPostValue(MainActivityModel.LOGIN_SUCCESS);
                mModel.loginState.seqPostValue(MainActivityModel.LOGIN_FINISH);
                User user = new User();
                user.username = userName;
                user.userpswd = userPswd;
                mModel.user.seqPostValue(user);
            }
        }).start();
    }
}

这样代码优雅很多。 可是你会问为什么要这样写代码呢?为什么有需要连续postvalue两次的需求?

因为我们每一次postvalue都必须要求ui进行更新,由于UI在主线程运行,那么post到主线程的runnable就能顺序执行。设计SUCCESS和FINISH两个状态的初衷是我在SUCCESS状态更新UI的时候弹出了Toast, 如果这个时候我旋转屏幕那么会导致Toast又弹出来一次, LiveData是会处理像屏幕旋转这种configuration change的,并且旋转过后根据状态更新UI,保证界面跟原来一样。

具体详见Demo吧 demo下载地址,5分

https://download.csdn.net/download/mtaxot/10519198


猜你喜欢

转载自blog.csdn.net/mtaxot/article/details/80909098