Mosby -- Android上的MVP框架

Mosby 是一个用于 Android 上的 MVP 框架,可以帮助大家在 Android 上通过 MVP 模式做出一个完善稳健、可重复使用的软件。MVP 模式由 MVC 模式衍生而来,下面先简单介绍一下这两种模式的定义和区别。

MVC 即 Model–View–Controller,是 Android 应用开发中最为常见的一种模式。它的结构图如下:

mvc

  • Model 可以理解为数据相关的模块,它不仅包含实体类的定义,还包含一切跟数据有关的操作(获取、解析、存储)。对于外部而言,它就是一个黑盒子,当外部需要获取数据时,只需要调用它提供的接口,至于里面究竟是怎么获取数据,从哪获取数据(内存、文件、数据库、网络),获取到的数据要怎么解析成实体对象等等,对外部来说是完全透明的,也不需要关心,这些都由 Model 内部进行处理,最终处理完成后只需要通过回调将最终的结果传递给外部即可。
  • View 是视图模块,主要包括界面上显示的各种 UI 控件和布局,它可以接受用户的操作事件并传递给 Controller,由 Controller 去触发 Model 层进行数据更新,更新完成后 Model 会通过回调的方式通知 View 更新界面。
  • Controller 是控制器,它主要起到一个连接 View 和 Model 的作用,View 持有 Controller,并通过 Controller 去更新 Model,而不直接拥有 Model,从而与数据模块进行解耦。

在 Android 开发中,Activity 通常充当了 View 和 Controller 两种角色,既负责界面的显示,又负责处理各种逻辑,导致代码十分庞大,由此衍生了 MVP 模式。在 MVP 模式中 Activity 可以简单的被看做 View,只需要负责界面的显示部分,其他的功能代码都提取到 Presenter 中进行处理,降低了其耦合度。

MVP 即 Model–View–Presenter,结构图如下:

mvp

MVP 中的 Model 和 View 跟 MVC 模式中类似,Presenter 也是充当了 View 和 Model 的中间人,但不同的是,Presenter 将 View 和 Model 完全分隔开了。View 拥有 Presenter 的实例。当 View 接收到用户的操作事件时,会调用 Presenter 进行处理,Presenter 通知 Model 更新数据。当 Model 数据更新完成后,不再通知 View,而是通知 Presenter,再由 Presenter 去调用 View 的方法更新界面。
另外,Presenter 与具体的 View 没有直接关联,而是通过定义好的接口 IView 进行交互。具体的 View 实现了 IView 接口,Presenter 中拥有 IView 对象而非具体的 View 对象,调用的也是 IView 中定义的抽象方法,从而使得在替换 View 的时候仍然可以重用 Presenter。View 不需要关心界面在什么时候更新,而只需实现 IView 中定义的更新方法即可,Presenter 会在需要的时候调用这些方法进行更新。

举个例子,当用户点击界面上的按钮想要加载数据时,View 会调用 Presenter 的 load 方法进行加载,此时如果需要出现加载动画则由 Presenter 调用 IView 中的 showLoading 方法显示动画,接着 Presenter 便会调用 Model 去加载数据,Model 内部会根据需要从网络或者本地加载数据并解析成 Presenter 需要的实体对象,再通过回调将结果传递给 Presenter,Presenter 收到结果后,调用 IView 的 showContent 方法更新界面,如果数据加载失败,则调用 IView 中的 showError 方法显示错误界面。
整个流程如下图所示:

流程

Mosby 就是一个这样的框架,它封装好了 MVP 框架所需要的类和逻辑,我们只需要简单的实现其提供的接口即可搭建一个 MVP 结构的应用,下面介绍一下它的使用和一些基本类。

1、在build.gradle文件中加入以下依赖:

dependencies {
    compile 'com.hannesdorfmann.mosby:mvp:2.0.1'
    compile 'com.hannesdorfmann.mosby:viewstate:2.0.1' // optional viewstate feature
}

2、核心类
Mosby 中所有 View 的基类是 MvpView,它是一个空的 interface 。Presenter 的基类是 MvpPresenter,这个接口里面只有两个方法attachView()detachView(),用于跟对应的 View 关联:

public interface MvpView {
}
public interface MvpPresenter<V extends MvpView> {
    public void attachView(V view);
    public void detachView(boolean retainInstance);
}

上文提到在 MVP 框架中 Activity 或 Fragment 都被视为普通的 View,与 FrameLayout 等一视同仁。
Mosby 提供了 MvpActivityMvpFragmentMvpFrameLayoutMvpLinearLayoutMvpRelativeLayout 等抽象类作为对应 View 的基类,这些抽象类都实现了 MvpView 接口。
一个 MvpView 会关联一个 MvpPresenter,并且管理 MvpPresenter 的生命周期。事实上,这些逻辑都已经在这些抽象类里面封装好了,在 MvpActivity 的onCreate()onDestroy()、MvpFragment 的onViewCreated()onDestroyView()、MvpFrameLayout 等的onAttachedToWindow()onDetachedFromWindow()中分别调用了createPresenter(); getPresenter().attachView();getPresenter().detachView();

MvpBasePresenter 实现了 MvpPresenter,为了避免内存泄露,它只持有 View 的弱引用,因此,使用之前需要先判断isViewAttached()并调用getView()来获取引用。

public class MvpBasePresenter<V extends MvpView> implements MvpPresenter<V> {
    private WeakReference<V> viewRef;
    @Override public void attachView(V view) {
        viewRef = new WeakReference<V>(view);
    }
    @Nullable public V getView() {
        return viewRef == null ? null : viewRef.get();
    }
    public boolean isViewAttached() {
        return viewRef != null && viewRef.get() != null;
    }
    @Override public void detachView(boolean retainInstance) {
        if (viewRef != null) {
            viewRef.clear();
            viewRef = null;
        }
    }
}

应用程序经常会做这样一件事:在后台加载数据,同时显示 LoadingView,加载完成时显示内容 Content,或加载失败时显示 ErrorView,我们把这样一个过程叫做 LCE(Loading-Content-Error)。为了避免重复执行这一工作流,Mosby 库提供了 MvpLceView

public interface MvpLceView<M> extends MvpView {
    public void showLoading(boolean pullToRefresh);
    public void showContent();
    public void showError(Throwable e, boolean pullToRefresh);
    public void setData(M data);
    public void loadData(boolean pullToRefresh);
}

同样,针对这一需求,Mosby 库还提供了 MvpLceActivityMvpLceFragment,两者都实现了 MvpLceView,并要求解析的 xml 布局包含 id 为R.id.loadingViewR.id.contentViewR.id.errorView的 View,其中R.id.errorView需要为 TextView。

我们先看一下 MvpLceFragment 的定义:

public abstract class MvpLceFragment<CV extends View, M, V extends MvpLceView<M>, P extends MvpPresenter<V>>  extends MvpFragment<V, P> implements MvpLceView<M>

可能大家第一眼就会被这么长一串定义给吓到,其实仔细分析一下并不难。这里面有四种类属参数:CV 代表的是 contentView 的类型,M 是指用来显示的数据类,V 即 View 接口,P 是 Presenter 的类型。

举个例子,假设我们的需求是加载并显示一组用户的信息,简单一点,每个用户信息里面就包含一个姓名字段。

首先需要定义一个数据类:

public class User {
    public String name;
}

由于我们要显示的是一组用户信息,而非一条,因此 M 就是 List<User>。

然后再定义一个我们自己的 View 接口,即 V

public interface UsersView extends MvpLceView<List<User>> {
}

实际上这里的 UsersView 并没有增加额外的方法,对于这种情况我们也可以直接使用 MvpLceView<List<User>> 作为 V,这里为了更清晰,还是定义一个 UsersView。

接下来就是 P 了:

public class UsersPresenter extends MvpBasePresenter<UsersView> {
    public interface OnDataLoadedListener {
        void onDataLoaded(List<User> users);
        void onDataLoadFailed(Throwable e);
    }
    public void loadData(final boolean pullToRefresh) {
        if (!isViewAttached()) {
            return;
        }
        getView().showLoading(pullToRefresh);
        ModelClient.loadUsers(new OnDataLoadedListener () {
            @Override
            public void onDataLoaded(List<User> users) {
                if (!isViewAttached()) {
                    return;
                }
                getView().setData(users);
                getView().showContent();
            }
            @Override
            public void onDataLoadFailed(Throwable e) {
                if (!isViewAttached()) {
                    return;
                }
                getView().showError(e, pullToRefresh);
            }
        });
    }
}

UsersPresenter 继承自 MvpBasePresenter,并增加了一个加载数据的方法loadData(),该方法首先调用isViewAttached()检查 View 是否连接到了 Presenter,接着调用getView().showLoading(pullToRefresh);显示 loadingView,并调用 Model 模块提供的ModelClient.loadUsers()方法去加载数据。
Preseneter 并不关心 Model 模块是怎么实现数据的加载的,仅需要传入加载完成时回调的listener即可。当数据加载完成后,Model 模块会根据成功或者失败回调listeneronDataLoaded()onDataLoadFailed()方法。
Presenter 在onDataLoaded()中调用getView().setData(users);getView().showContent();更新数据并显示内容,在onDataLoadFailed()中调用getView().showError(e, pullToRefresh);显示 errorView。

当这些定义好后,就可以定义真正的 View 了,也就是 UsersFragment。
首先我们要完成布局文件 users_fragment.xml 的定义,这是一个拥有 loadingView、contentView、errorView 的布局,并且需要支持下拉刷新,因此我们采用 SwipeRefreshLayout 作为 contentView,里面再包含一个 RecyclerView 用于显示列表:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ProgressBar
        android:id="@+id/loadingView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:indeterminate="true"/>
    <TextView
        android:id="@+id/errorView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:drawableTop="@drawable/ic_error"
        android:gravity="center"/>
    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/contentView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout>

因此,我们之前提到的 MvpLceFragment 中的四种类属参数分别就是 SwipeRefreshLayoutList<User>UsersViewUsersPresenter,我们的 UsersFragment 继承自 MvpLceFragment 并实现 UsersView 接口:

public class UsersFragment extends MvpLceFragment<SwipeRefreshLayout, List<User>, UsersView, UsersPresenter> implements UsersView {

    @Bind(R.id.recyclerView)
    RecyclerView mRecyclerView;

    UsersAdapter mAdapter;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.users_fragment, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        ButterKnife.bind(this, view);

        contentView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                loadData(true);
            }
        });

        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        mAdapter = new UsersAdatper(getActivity);
        mRecyclerView.setAdapter(mAdapter);

        loadData(false);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        ButterKnife.unbind(this);
    }

    @Override
    public UsersPresenter createPresenter() {
        return new UsersPresenter();
    }

    @Override
    protected String getErrorMessage(Throwable e, boolean pullToRefresh) {
        return pullToRefresh ? "load data error" : "load data error retry";
    }

    @Override
    public void loadData(boolean pullToRefresh) {
        presenter.loadData(pullToRefresh);
    }

    @Override
    public void setData(List<User> data) {
        mAdapter.setData(data);
        mAdapter.notifyDataSetChanged();
    }

    @Override
    public void showLoading(boolean pullToRefresh) {
        super.showLoading(pullToRefresh);
        contentView.setRefreshing(true);
    }

    @Override
    public void showContent() {
        super.showContent();
        contentView.setRefreshing(false);
    }

    @Override
    public void showError(Throwable e, boolean pullToRefresh) {
        super.showError(e, pullToRefresh);
        contentView.setRefreshing(false);
    }
}

UsersFragment 里面主要是实现基类的抽象方法:

  • createPresenter()用于创建对应的 Presenter,至于 Presenter 与 UsersFragment 的关联以及生命周期管理都在父类中封装好了。
  • getErrorMessage()返回加载失败时的提示文案,文案的使用也在父类中处理好了,如果是下拉刷新,则直接弹提示,否则,将文案设为 errorView 的 Text,并支持点击重新加载。
  • loadData()调用 Presenter 的loadData()方法即可。
  • setData()showContent()在数据加载成功时由 Presenter 去调用,这里面只需要处理自己的数据更新和界面显示即可。
  • showLoading()showError()也只在加载时和加载失败时由 Presenter 调用,这里也只需要更新自己的界面即可。

总的来说,Mvp 模式中 View 的功能十分简单,基本上不用关心数据加载的过程和逻辑,只需要调用 Presenter 去加载数据即可,之后也不用管什么时候去更新界面,只需要提供更新界面的方法给 Presenter 调用即可。

至于里面的 UsersAdapter 和 ModelClient,这里就不写实现了,对于 UsersAdapter 来说,推荐一个非常好用的库 AnnotatedAdapter,只需要写非常少的代码就能帮我们生成 RecyclerView 和 AbsListView 的适配器,项目主页为 https://github.com/sockeqwe/AnnotatedAdapter 大家可以看看里面的介绍。对于 ModelClient 来说网络请求推荐使用 RetrofitRxJava

上面的例子通过请求一个 List<User> 的功能介绍了 Mosby 框架的核心类和使用,其实我们应用中往往会请求各种数据,基本上都是 LCE 的流程,因此,可以考虑将上面的代码再抽象一下,将 List<User> 改为通用的数据类型 M,将跟具体的数据相关的部分去掉,交给具体的实现类去做,从而将公共的部分提取出来通用。以下便是抽取后的代码。

首先是 LcePresenter,这里在请求数据时还支持传入参数,具体的实现类需要实现loadData(Map<String, Object> params, OnDataLoadedListener listener)方法,里面调用 Model 提供的对应的方法加载数据即可:

public abstract class LcePresenter<M> extends MvpBasePresenter<MvpLceView<M>> {

    public interface OnDataLoadedListener {
        void onDataLoaded(Object data);

        void onDataLoadFailed(Throwable e);
    }

    public void loadData(final boolean pullToRefresh, final Map<String, Object> params) {
        if (!isViewAttached()) {
            return;
        }
        getView().showLoading(pullToRefresh);
        loadData(params, new OnDataLoadedListener() {
            @Override
            public void onDataLoaded(Object data) {
                if (!isViewAttached()) {
                    return;
                }
                getView().setData((M) data);
                getView().showContent();
            }

            @Override
            public void onDataLoadFailed(Throwable e) {
                if (!isViewAttached()) {
                    return;
                }
                getView().showError(e, pullToRefresh);
            }
        });
    }

    protected abstract void loadData(Map<String, Object> params, OnDataLoadedListener listener);
}

然后是 LceFragment,这里的布局文件 lce_fragment.xml,跟之前的 user_fragment.xml 一样。支持 loadMore 实现分页加载,外部可以设置是否开启 loadMore 功能,默认是开启,如果需要的话也可以设置 params:

public abstract class LceFragment<M> extends MvpLceFragment<SwipeRefreshLayout, M, MvpLceView<M>, LcePresenter<M>> {

    private static final int DEFAULT_PAGE_COUNT = 10;

    protected int mPageNum = 1;

    protected LceAdapter mAdapter;
    protected Map<String, Object> mParams = new HashMap<>();
    protected boolean mLoadMoreEnabled = true;

    @Bind(R.id.recyclerView)
    RecyclerView mRecyclerView;

    public LceFragment setParams(Map<String, Object> params) {
        mParams.putAll(params);
        return this;
    }

    public LceFragment setLoadMoreEnabled(boolean enabled) {
        mLoadMoreEnabled = enabled;
        return this;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.lce_fragment, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        ButterKnife.bind(this, view);

        contentView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                mPageNum = 1;
                loadData(true);
            }
        });

        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        if (mLoadMoreEnabled) {
            mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
                boolean isSlidingToLast = false;

                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager();
                    if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                        int lastVisibleItem = manager.findLastCompletelyVisibleItemPosition();
                        int totalItemCount = manager.getItemCount();
                        if (lastVisibleItem == (totalItemCount - 1) && isSlidingToLast && !contentView.isRefreshing()) {
                            mPageNum++;
                            loadData(true);
                        }
                    }
                }

                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    super.onScrolled(recyclerView, dx, dy);
                    if (dy > 0) {
                        isSlidingToLast = true;
                    } else {
                        isSlidingToLast = false;
                    }
                }
            });
        }
        mAdapter = createAdapter();
        mRecyclerView.setAdapter(mAdapter);

        loadData(false);
    }

    protected abstract LceAdapter createAdapter();

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        ButterKnife.unbind(this);
    }

    @Override
    protected String getErrorMessage(Throwable e, boolean pullToRefresh) {
        return pullToRefresh ? "load data error" : "load data error retry";
    }

    @Override
    public void loadData(boolean pullToRefresh) {
        if (mLoadMoreEnabled) {
            mParams.put(ApiParamKeys.PAGE_NUM, mPageNum);
            if (mParams.get(ApiParamKeys.PAGE_COUNT) == null) {
                mParams.put(ApiParamKeys.PAGE_COUNT, DEFAULT_PAGE_COUNT);
            }
        }
        presenter.loadData(pullToRefresh, mParams);
    }

    @Override
    public void showLoading(boolean pullToRefresh) {
        super.showLoading(pullToRefresh);
        contentView.setRefreshing(true);
    }

    @Override
    public void showContent() {
        super.showContent();
        contentView.setRefreshing(false);
    }

    @Override
    public void showError(Throwable e, boolean pullToRefresh) {
        super.showError(e, pullToRefresh);
        contentView.setRefreshing(false);
    }
}

这里的 LceAdapter 使用了前面提到的 AnnotatedAdapter 库,继承自 SupportAnnotatedAdapter:

public abstract class LceAdapter<M> extends SupportAnnotatedAdapter {

    protected Context mContext;
    protected M mData;

    public LceAdapter(Context context) {
        super(context);
        mContext = context;
    }

    public void setData(M data) {
        mData = data;
    }
}

将这三个类抽象出来后,我们再来看看请求 List<User> 该怎么实现。
首先是 UsersPresenter,ModelClient 是 Model 模块提供的类,这里暂时不用关心:

public class UsersPresenter extends LcePresenter<List<User>> {

    @Override
    protected void loadData(Map<String, Object> params, OnDataLoadedListener listener) {
        ModelClient.requestUsers(params, listener);
    }
}

然后是 UsersFragment,这里如果是上拉加载更多则将数据添加到列表后面,如果是下拉刷新,则先清空列表再将数据加入列表:

public class UsersFragment extends LceFragment<List<User>> {

    private List<User> mData = new ArrayList<>();

    @Override
    public LcePresenter<List<User>> createPresenter() {
        return new UsersPresenter();
    }

    @Override
    protected LceAdapter createAdapter() {
        return new UsersAdapter(getActivity());
    }

    @Override
    public void setData(List<User> data) {
        if (mPageNum == 1) {
            mData.clear();
        }
        mData.addAll(data);
        mAdapter.setData(mData);
        mAdapter.notifyDataSetChanged();
    }
}

再看一下 UsersAdapter,user_item_view.xml 是列表中每一项的布局,里面仅有一个 id 为 R.id.name 的 TextView 用于显示用户的名字:

public class UsersAdapter extends LceAdapter<List<User>> implements UsersAdapterBinder {

    @ViewType(
            layout = R.layout.user_item_view,
            views = {
                    @ViewField(id = R.id.name, name = "name", type = TextView.class),
            })
    public final int VIEW_TYPE_USER = 0;

    public UsersAdapter(Context context) {
        super(context);
    }

    @Override
    public int getItemCount() {
        return mData.size();
    }

    @Override
    public void bindViewHolder(UsersAdapterHolders.VIEW_TYPE_USERViewHolder vh, int position) {
        User user = mData.get(position);
        vh.name.setText(user.name);
    }
}

然后我们使用的时候在 Activity 里面调用:

Fragment fragment = new UsersFragment();
getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, fragment).commit();

是不是超级简单?可能有人会觉得你这里用了 RecyclerView,是不是只适用于请求一个 List 的情况?其实即使我们请求的数据不是一个 List 也不会有影响,就当做这个 RecyclerView 只显示一项并且禁止loadMore不就行了。比如我们请求的数据是 User,同样的,首先是 UserPresenter,注意这里都没有 s 了,M 由 List<User> 变成了 User:

public class UserPresenter extends LcePresenter<User> {

    @Override
    protected void loadData(Map<String, Object> params, OnDataLoadedListener listener) {
        ModelClient.requestUser(params, listener);
    }
}

然后是 UserFragment,由于这里不再是列表,直接将请求到的数据 set 进去即可:

public class UserFragment extends LceFragment<User> {

    @Override
    public LcePresenter<User> createPresenter() {
        return new UserPresenter();
    }

    @Override
    protected LceAdapter createAdapter() {
        return new UserAdapter(getActivity());
    }

    @Override
    public void setData(User data) {
        mAdapter.setData(data);
        mAdapter.notifyDataSetChanged();
    }
}

接下来是 UserAdapter

public class UserAdapter extends LceAdapter<User> implements UserAdapterBinder {

    @ViewType(
            layout = R.layout.user_item_view,
            views = {
                    @ViewField(id = R.id.name, name = "name", type = TextView.class),
            })
    public final int VIEW_TYPE_USER = 0;

    public UserAdapter(Context context) {
        super(context);
    }

    @Override
    public int getItemCount() {
        return 1;
    }

    @Override
    public void bindViewHolder(UserAdapterHolders.VIEW_TYPE_USERViewHolder vh, int position) {
        vh.name.setText(mData.name);
    }
}

然后我们使用的时候在 Activity 里面调用:

Fragment fragment = new UserFragment().setLoadMoreEnabled(false);
getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, fragment).commit();

这里禁掉了 loadMore,同样也很简单。

如果我们项目中需要频繁的用到 LCE 的流程,可以将上面抽出来的三个类 LcePresenter、LceFragment、LceAdapter 拷到项目中,之后每个需要实现 LCE 的需求都继承这三个抽象类,只需要编写少量代码即可完成。

当然,对于那种非列表且不需要下拉刷新也不用loadMore的布局,可以将 lce_fragment.xml 里面的 SwipeRefreshLayout 和 RecyclerView 去掉,以 FrameLayout 做为 contentView,同时将 LceFragment 里面相关的代码去掉,在 setData 时将真正的布局 add 到 contentView 里面即可。

推荐阅读更多精彩内容