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