MVP初步实践

问题的引出

  对于一个基本的Android项目,我们在初学的时候的做法通常都是直接在xml中绘制界面,然后在对应的Activity中做一些响应操作。这样做在一些demo演示的时候倒是没什么问题,但是一旦项目稍微有点规模,就会导致Activity变得臃肿。而对于一个成百上千行的Activity,当我们需要对界面进行修改的时候,会很难快速定位到对应修改的地方,并且由于所有的操作都在activity中,耦合度很高,当修改某一处的时候,可能会涉及到多个方面,从而需要修改多处。

  解决这个问题的方法就是将在activity中的操作进行分离,同时对于整个项目也进行功能模块的分离。这样的话,当我们需要修改某处的时候,只用修改对应的部分即可。因此就有了mvp模式了,虽然mvp模式也存在很多缺点,巨多的接口,但是对于一个中小型项目,这点缺点还是可以忽略的。

MVP

  而MVP模式指的就是Model,View,Presenter这三者。也就是说MVP模式将集中在Activity中的操作拆分成了三个部分,由这三个部分共同完成一个操作。

  其中,model是获取数据的部分,它只负责获取数据,而不进行其他操作。View则是界面显示的部分,同样的,它只负责展示界面,不进行其他操作,由Activity/Fragment担任。Presenter是连接model和View的部分,它是主要负责进行逻辑的处理。

MVp

  因此,一个完整的事件流程理应是这样:在View中产生某个事件,然后由View转交给Presenter,Presenter进行逻辑的处理后通知model获取数据,在Model获取完数据后回调给Presenter,然后presenter将数据进行处理后交给View去显示。

在这个过程中,可以看出View与Model是完全没有联系的,它们之间的数据通过Presenter进行互通。另外,Model获取到的数据也是通过回调接口来提供给Presenter的。这样,就可以将Model独立出来,减少数据的耦合。
  所以他们的关系也应当如上图所示,View与Presenter互相持有,可以互相通信。而Model只被Presenter持有,是单向通信的。

小结

  对于MVP而言,数据的获取和界面的显示是没有关系的。也就是说,当我们需求进行变化的时候,就可以根据需求而只修改其中的一部分,而不用全部修改。
  另外,由于逻辑与界面的显示是分离的,当我们测试的时候就可以不用对Activity进行注释修改添加,而是可以直接新建一个Presenter继承原Presenter,然后进行修改,极大的方便了我们测试。

  至于MVP的实现,也不是一成不变的,而是根据自己的习惯或者需求进行一些变动。毕竟MVP模式本身的引出就是为了解耦,分割各个模块的。

实现MVP

  对于一个应用程序,我们应当保持其界面的一致性,因此这里先定义了三个接口,分别对应MVP的三个部分的行为准则,这是三个接口中,定义的是每组MVP都应当拥有的功能,也就是说每组MVP都应当实现这些对应的方法。对于MVP,当然应该至少有三个类的实现,分别对应MVP的每个部分,称为一组MVP。
  既然说到了这里,就有必要说一下分包的实现了。有些人习惯建立三个包Model、View、Presenter,然后将对应的文件分别放在这三个包中。还有些人习惯按照功能分包,然后将每个功能对应的MVP放在一起。这里我就是采用的后者,按照功能分包,同时加入一个Contract来协同每组MVP的行为。

包结构

  这里,添加了一个Module,这个Module是专门用于放置MVP基础的搭建的,叫做common。然后还有一个Module,叫做app,是具体的示例程序对应的Module。
包结构

具体实现

  首先是View。对于View而言,这里只添加了两个方法,进度条的显示和隐藏。当进行某项操作的时候,应当显示进度条,然后操作结束的时候隐藏,这样才能使得应用更加友好。

public interface View {
    /**
     * 显示进度条,用于提示正在加载
     */
    void showProgress();
    /**
     * 隐藏进度条
     */
    void hideProgress();
}

  接下来是model的接口,由于这里model只是负责获取数据的部分,它是独立的。因而没有什么公共的方法,但是为了后面的model的统一,这里还是创建了一个model接口,只是其中并没有方法而已。

public interface Model {
}

  最后就差Presenter了,而Presenter是model和view的连接者,它是需要同时持有model和view。但是对于View而言,它的生命周期很是复杂,我们需要当view创建的时候连接二者,而view销毁的时候解开连接。否则的话当view销毁,但是还被Presenter持有的时候,view就不会被回收,从而造成内存的泄露。那么presenter就应当拥有解除关联和判断是否还在关联的方法。

public interface Presenter {
    /**
     * 解除关联
     */
    void detach();

    /**
     * 判断View是否与当前的Presenter建立连接
     *
     * @return true为已建立连接
     */
    boolean isAttach();

    /**
     * 检查是否与View建立连接
     */
    void checkAttach();
}

  对于每个model,view,presenter,都应当实现以上的接口。而对于这种共有的方法,每个对应的类都去实现一遍显然是不合适的,最好的做法是使用一个基类来实现这些方法,其余的类只要继承这个基类即可拥有这些方法了。同时也会因为这些方法都是在基类中实现的,从而会使得每个类的行为表现得具有一致性。

  要知道的是,上面只是定义了三个接口,只是代表每组MVP的行为而已。实际上还并没有实现MVP的搭建,那么下面,将会通过Base基类搭建一个MVP模式基础。

Model
public class BaseModel implements Model {
}

  因为model没有共有的方法,所以它的基类也是一个空的类。可能从这里看的话会显得多余,毕竟只是一个空的类,那么就完全没必要去创建它。而实际上也的确没必要去创建这个BaseModel,毕竟接口Model本身就是为了实现model的一致性了。而之所以这里又重新创建了这个Base类,是因为后面将会基于该mvp框架搭建RxJava+Retrofit实现的MVP,而引入BaseModel的话,不仅能够使得两种MVP的使用具有一致性,同时也可以在BaseModel中添加model可能需要的共同的方法。
  那么接下来看Presenter的实现。

Presenter
public abstract class BasePresenter<V extends View, M extends Model> implements Presenter {
    protected V mView;
    protected M mModel;

    public BasePresenter(V view) {
        mView = view;
        mModel = createModel();
    }

    @Override
    public void detach() {
        mView = null;
    }

    @Override
    public boolean isAttach() {
        return mView != null;
    }

    @Override
    public void checkAttach() {
        if (!isAttach()){
            throw new RuntimeException("当前Presenter未与View建立连接");
        }
    }

    /**
     * 创建Presenter对应的Model
     *
     * @return Model
     */
    protected abstract M createModel();
}

  从上面可以看到,这里presenter实现了presenter接口的共有方法,同时还定义了两个变量,mView 和mModel ,也就是mvp中的另外两者。并且只提供了一个构造方法,在这个构造方法中完成和View的绑定。这里就可以知道,我们没在presenter的接口中定义绑定的方法,是因为将绑定的过程放到了构造方法,也就是说当Presenter存在的时候就需要绑定View。
  另外,presenter中并没有直接引用model,而是提供了一个抽象方法来生成model。因为对于一个presenter而言,model并不是必须存在的,只有当需要获取数据的时候才会需要model,因此这里将model的创建放在了一个抽象方法中,当我们不需要的时候直接返回null即可。

  接下来是View的基类了,因为view是由Activity和Fragment担任的,所以这里将会分成两个BaseView。

View
public abstract class BaseActivity<P extends Presenter> extends AppCompatActivity implements View {
    protected P mPresenter;
    protected Context mContext;
    private Dialog mProgressDialog;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getContentView());
        mPresenter = createPresenter();
        initView();
        mContext = this;
        mProgressDialog = DialogUtils.getDefaultDialog(mContext);
    }

    @Override
    protected void onDestroy() {
        mPresenter.detach();
        super.onDestroy();
    }

    @Override
    public void showProgress() {
        if (mProgressDialog != null && !mProgressDialog.isShowing()) {
            mProgressDialog.show();
        }
    }

    @Override
    public void hideProgress() {
        if (mProgressDialog.isShowing() && mProgressDialog != null) {
            mProgressDialog.dismiss();
        }
    }

    /**
     * 返回Content布局id
     *
     * @return 布局文件id
     */
    protected abstract @LayoutRes int getContentView();

    /**
     * View的初始化工作
     */
    protected abstract void initView();

    /**
     * 创建Presenter
     *
     * @return 与Activity关联的Presenter
     */
    protected abstract P createPresenter();
}

  上面是使用Activity作为View而实现的基础View。在其中的onCreate中创建presenter和进度条,并且在onDestroy中解除了和Presenter的绑定。另外除了实现基本的显示隐藏进度条外,还抽象出了三个方法。分别是contentView的布局xml的ID和初始化的方法以及创建Presenter的方法。这样的话,当具体的View继承该基类的时候只需要实现这三个方法即可,而不用去考虑其生命周期的变化了。
  此外,为了对进度条的统一管理,将生成进度条的方法放在了DialogUtils中,这样当需要修改进度条的样式的时候将会更加方便。

public class DialogUtils {

    /**
     * 用于生成默认的正在加载的进度条,该进度条用于普通加载时显示
     *
     * @return 对话框
     */
    public static Dialog getDefaultDialog(Context context) {
        Dialog dialog = new ProgressDialog(context);
        dialog.setCancelable(true);
        dialog.setCanceledOnTouchOutside(false);
        return dialog;
    }
}

  对于Fragment担任的View与Activity也是一样,唯一的区别就是他们生命周期的不同,从而在不同的方法中进行Presenter的绑定和解除。

public abstract class BaseFragment<P extends Presenter> extends Fragment implements com.pgaofeng.common.mvp.View {

    protected P mPresenter;
    private Dialog mDialog;
    protected Context mContext;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(getContentView(), container, false);
        initView(view);
        mPresenter = createPresenter();
        mContext = getContext();
        mDialog = DialogUtils.getDefaultDialog(mContext);
        return view;
    }

    @Override
    public void onDestroyView() {
        mPresenter.detach();
        super.onDestroyView();
    }

    @Override
    public void showProgress() {
        if (mDialog != null && !mDialog.isShowing()) {
            mDialog.show();
        }
    }

    @Override
    public void hideProgress() {
        if (mDialog != null && mDialog.isShowing()) {
            mDialog.dismiss();
        }
    }

    /**
     * 获取content布局id
     *
     * @return 布局id
     */
    protected abstract @LayoutRes int getContentView();

    /**
     * 初始化View
     *
     * @param view contentView
     */
    protected abstract void initView(View view);

    /**
     * 创建View对应的Presenter
     *
     * @return Presenter
     */
    protected abstract P createPresenter();

}

  至此,对于基础MVP的封装已经完成。也就是说,MVP中的三者已经通过这几个Base类连接起来了,因此我们的封装到这里已经算是完成了。只是完成了MVP的封装又怎么能满足我们的要求呢,接下来当然是要具体的通过实际案例来体验一下封装的MVP如何使用了。


案例(使用方式)

  下面将通过一个小的案例来演示如何使用MVP。这里的案例就是通过点击一个按钮实现获取数据,然后由textView展示。
先看一下xml的布局。

xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".main.view.MainActivity">
    <TextView
        android:id="@+id/tv_myText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
    <Button
        android:id="@+id/btn_update"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="更新" />
</LinearLayout>

  对于一个功能,MVP的各个部分都应当有相应的具体的行为,而行为应当是使用接口来提取的,但是若每组MVP都提出三个接口的话,会使得文件更加的多了。因此,这里在使用MVP的时候,使用了Contract来合并了每组的mvp接口。

Contract
public interface MainContract {
    interface View {
        /**
         * 更新TextView的text
         *
         * @param text 内容
         */
        void updateText(String text);

        /**
         * 显示Toast
         *
         * @param message 消息内容
         */
        void showToast(String message);
    }

    interface Presenter {
        /**
         * 更新TextView的内容
         */
        void updateTextViewText();
    }

    interface Model {
        /**
         * 获取TextView 的内容
         *
         * @param param    请求参数
         * @param callBack 请求回调
         * @param handler  用于更新主线程UI
         */
        void getTextString(String param, ModelCallBack callBack, Handler handler);
    }
}

  从上面可以看到的是,Contract接口组合了每一组MVP的所有接口,这些接口都应当是它们的行为准则。在Modle中,获取数据的方法中传入了一个回调参数,这个参数是在Model获取到数据后回调的,由于Presenter和Model是单向通信,因此只能使用回调的方式来将结果传回Presenter。

View
public class MainActivity extends BaseActivity<MainPresenter> implements MainContract.View {

    TextView mTextView;
    Button mButton;

    @Override
    protected int getContentView() {
        return R.layout.activity_main;
    }

    @Override
    protected void initView() {
        mTextView = findViewById(R.id.tv_myText);
        mButton = findViewById(R.id.btn_update);
        mButton.setOnClickListener(v -> {
            mPresenter.updateTextViewText();
        });
    }

    @Override
    protected MainPresenter createPresenter() {
        return new MainPresenter(this,new Handler());
    }

    @Override
    public void updateText(String text) {
        mTextView.setText(text);
    }

    @Override
    public void showToast(String message) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
    }
}

  MainActivity实现了View的接口,同时继承的是前面封装的BaseActivity。可以看到的是,Activity已经很简洁了,它其中的一些方法除了必要的一些外就只剩下操作界面UI的了,这与我们的期望是一致的,View只负责显示UI即可。
  另外,引入的泛型是MainPresenter,也就是说该View持有的Presenter就是MainPresenter。这样的话,就可以根据用户的需求去主动地调用Presenter的方法了,这里是通过按钮的点击来触发的更新text操作。

Presenter
public class MainPresenter extends BasePresenter<MainActivity, MainModel> implements MainContract.Presenter {
    public MainPresenter(MainActivity view, Handler handler) {
        super(view);
        this.mHandler = handler;
    }

    private String[] params = {"success", "fail"};
    private Handler mHandler;
    private Random random = new Random(System.currentTimeMillis());

    @Override
    protected MainModel createModel() {
        return new MainModel();
    }
    
    @Override
    public void updateTextViewText() {
        mView.showProgress();
        // 模拟获取数据的几种状态,这里通过参数来决定是否获取数据成功
        int index = random.nextInt(2);

        mModel.getTextString(params[index], new ModelCallBack() {
            @Override
            public void success(BaseData<?> baseData) {
                checkAttach();
                String s = (String) baseData.getData();
                if (!TextUtils.isEmpty(s)) {
                    mView.updateText(s);
                }
                mView.hideProgress();
                mView.showToast(baseData.getMessage());
            }

            @Override
            public void fail(Throwable throwable) {

                checkAttach();
                mView.showToast("获取数据失败!");
                mView.hideProgress();
            }
        }, mHandler);
    }
}

  而MainPresenter中只实现了一个方法,也就是Contract中的presenter中定义的方法。这里模拟的是获取数据,但是由于我们并没有真实的数据可以获取,这里就使用随机来模拟了两种情况,成功和失败这两种情况。
  同时在构造方法中传入了一个Handler,之所以使用handler,是因为通常获取数据是一个耗时的操作,因此应该在子线程中进行。而界面的操作则必须在UI线程中进行,因此这里使用Handler来操作UI。而从上面可以看出在Presenter中还是调用了Model的获取数据的,其中由于获取数据可能是个耗时,因此Model的方法参数传入的除了参数外还有一个回调和Handler,然后在Model获取完数据后将会调用这个方法将结果传回给Presenter。
  而之所以把Handler也传递给了Model,是因为这里分配职责的时候,把线程的切换交给了Model来处理。也就是说,Model如何获取数据以及在什么线程获取都是由它自己决定,而Presenter只需要它在UI线程中将结果回调回来即可。

CallBack
public interface ModelCallBack {
    /**
     * 请求成功的回调
     *
     * @param baseData 返回值的基础bean
     */
    void success(BaseData<?> baseData);

    /**
     * 请求失败的回调
     *
     * @param throwable 错误异常信息
     */
    void fail(Throwable throwable);
}

  这里定义回调接口的时候,已经限定了获取成功的回调参数。也就是说,Model必须要将数据转换成我们所需的格式再回调回来。

BaseData
public class BaseData<T> {
    /**
     * 返回码
     */
    private int code;
    /**
     * 返回提示消息
     */
    private String message;
    /**
     * 返回的实际数据内容
     */
    private T data;
	...
	// getter,setter  
}

  这里可以看到,BaseData封装了三个参数,code,Message和Data。其中code和message是对于此次获取的数据的一个描述,其中code应当是与后台协议后的返回码,而message则是对于code的一些描述消息。data使用的是泛型,是真正存储数据的地方。
  那么继续看Model。

Model
public class MainModel extends BaseModel implements MainContract.Model {
    @Override
    public void getTextString(final String param, final ModelCallBack callBack, Handler handler) {
        if (callBack == null) {
            throw new RuntimeException("回调不应为空");
        }
        new Thread(() -> {
            try {
                // 模拟在子线程中获取数据
                Thread.sleep(2000);
                // 获取数据结束结束后切换主线程回调
                handler.post(() -> {
                    BaseData<String> baseData = new BaseData<>();
                    switch (param) {
                        case "success":
                            baseData.setCode(0);
                            baseData.setMessage("获取成功");
                            baseData.setData("我是获取到的消息内容");
                            callBack.success(baseData);
                            break;
                        case "fail":
                            callBack.fail(new RuntimeException("人为异常!"));
                            break;
                        default:
                            break;
                    }
                });

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

    }
}

  Model只有一个方法,就是获取数据的那个方法。其中开辟了一个子线程,然后在线程中sleep了2秒作为获取数据的耗时。然后通过handler在UI线程把数据传递给了Presenter,最后Presenter会通知View去展示这个数据。

总结

  MVP模式主要的作用就是解耦,完全是为了降低耦合度发展而来的一种编码模式。将界面的展示和数据的获取进行分离,会使得整个项目的结构更加清晰。这样不仅能在需求修改的时候及时定位到目标,还能更加方便得进行功能模块测试。而这样的分包分模块更是方便团队协作,以及新人对项目的接手难度。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值