Google官方Mvp架构详解(基于仿今日头条News项目)

先来一发Google官方Mvp架构地址:
https://github.com/googlesamples/android-architecture/tree/todo-mvp/

基类介绍

BaseFragment

这个类是Fragment的一级父类。主要完成以下功能:

  1. 在类的声明上添加了一个IBasePresenter的泛型,并且实现了IBaseView接口。

  2. 统一封装了EventBus的使用,在基类中注册和反注册EventBus。子类如果需要使用EventBus,则只需要添加相应的注解即可。(关于通过注解的方式在基类中封装EventBus,可以参考https://blog.csdn.net/xieluoxixi/article/details/78262765)

  3. 在Fragment挂载在Activity的时候,保存了一个上下文Context对象。

  4. 在onCreate()方法中,调用了setPresenter()方法,来给Fragment设置它对应的Presenter,并且先于onCreateView()方法执行。(因为可能会在initData()、initView()、initEvent()方法中用到Presenter对象,避免空指针异常)

package com.example.think.base;

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.example.think.utils.BindEventBus;

import org.greenrobot.eventbus.EventBus;

import butterknife.ButterKnife;

/**
 * Author: Funny
 * Description: This is 一级Fragment
 */
public abstract class BaseFragment

extends Fragment implements IBaseView

{ protected P mPresenter; protected Context mContext; @Override public void onAttach(Context context) { super.onAttach(context); this.mContext = context; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); //设置Presenter对象 setPresenter(mPresenter); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = LayoutInflater.from(getContext()).inflate(getLayoutId(), null); ButterKnife.bind(this, view); registEventBus(); initData(); initView(view); initEvent(); return view; } private void registEventBus() { if (this.getClass().isAnnotationPresent(BindEventBus.class)) { EventBus.getDefault().register(this); } } @Override public void onDestroy() { super.onDestroy(); if (this.getClass().isAnnotationPresent(BindEventBus.class)) { EventBus.getDefault().unregister(this); } } protected abstract int getLayoutId(); protected void initData() { } protected void initView(View view) { } protected void initEvent() { } }

IBasePresenter和IBaseView

接下来再来看看IBasePresenter和IBaseView里面的具体实现。这两个接口是MVP模式中Presenter和View的一级接口。其他的Presenter和View必须直接或者间接实现它。

这里有一个方法命名的小技巧,Presenter里面的方法全是do开头,而View里面的方法是on开头(想想也挺符合逻辑的,Presenter是干事的人,使劲的do,而View是被干的人,所以要on.......)。这样通过名字就可以知道是谁的方法,因为后面Activity或者Fragment里面的overrider方法太多,容易搞懵。

  • IBasePresenter

IBasePresenter里面有两个抽象方法,用于刷新数据和显示网络错误。这个功能方法和具体的业务逻辑有关,因为所有的Presenter都有这两个功能,其他更具体的功能,后面在它的子类中会说到。

package com.example.think.base;

/**
 * Author: Funny
 * Description: This is 一级Presenter接口
 */
public interface IBasePresenter {

    /**
     * 刷新数据
     */
    void doRefresh();

    /**
     * 显示网络错误
     */
    void doShowNetError();

}

  • IBaseView

IBaseView里面的方法和具体的业务逻辑有关,注释已经写得很清楚了

package com.example.think.base;

/**
 * Author: Funny
 * Description: This is 一级View接口
 */
public interface IBaseView

{ /** * 显示加载动画 */ void onShowLoading(); /** * 隐藏加载 */ void onHideLoading(); /** * 显示网络错误 */ void onShowNetError(); /** * 设置 presenter */ void setPresenter(P presenter); /** * 绑定生命周期 */ // AutoDisposeConverter bindAutoDispose(); }

BaseListFragment

BaseListFragment是BaseFragment的子类。所有列表页Fragment都继承它。

BaseListFragment所继承的LazyLoadFragment是处理懒加载的,LazyLoadFragment同样继承自BaseFragment。只需要注意一个地方,在它的生命周期方法onActivityCreated()中,执行了一个抽象方法fetchData(),这个抽象方法就是界面加载数据的开始。

package com.example.think.base;

import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Toast;

import com.example.think.R;

import me.drakeet.multitype.Items;
import me.drakeet.multitype.MultiTypeAdapter;

/**
 * Author: Funny
 * Description: This is BaseListFragment
 */
public abstract class BaseListFragment

extends LazyLoadFragment

implements IBaseListView

,SwipeRefreshLayout.OnRefreshListener{ protected RecyclerView mRecyclerView; protected SwipeRefreshLayout mRefreshLayout; protected MultiTypeAdapter mAdapter; protected boolean canLoadMore = false; @Override protected int getLayoutId() { return R.layout.fragment_list; } @Override protected void initView(View view) { mRecyclerView = view.findViewById(R.id.recycler_view); mRefreshLayout = view.findViewById(R.id.refresh_layout); mRecyclerView.setHasFixedSize(true); LinearLayoutManager manager = new LinearLayoutManager(getContext()); mRecyclerView.setLayoutManager(manager); //下拉刷新 mRefreshLayout.setOnRefreshListener(this); } @Override public void onShowLoading() { /** * 列表Fragment,显示加载视图,设置mRefreshLayout的刷新状态为true */ mRefreshLayout.post(() -> { mRefreshLayout.setRefreshing(true); }); } @Override public void onHideLoading() { /** * 列表Fragment,隐藏加载视图,设置mRefreshLayout刷新状态为false */ mRefreshLayout.post(() -> { mRefreshLayout.setRefreshing(false); }); } @Override public void onShowNetError() { /** * 列表Fragment,加载时显示网络错误 */ Toast.makeText(getContext(), "网络不给力", Toast.LENGTH_SHORT).show(); mAdapter.setItems(new Items()); mAdapter.notifyDataSetChanged(); canLoadMore = false; } @Override public void onShowNoMore() { /** * 列表Fragment,加载完毕,无更多数据 */ // TODO: 2018/9/6 无更多数据实现 canLoadMore = false; } @Override public void onRefresh() { LinearLayoutManager manager = (LinearLayoutManager) mRecyclerView.getLayoutManager(); int firstVisibleItemPosition = manager.findFirstVisibleItemPosition(); if (firstVisibleItemPosition == 0) { mPresenter.doRefresh(); return; } mRecyclerView.scrollToPosition(5); mRecyclerView.smoothScrollToPosition(0); } }

这里主要关注一下它所实现的IBaseListView接口,MVP模式有一个特点,一个View所需要完成的功能方法,往往都定义在它实现的View接口中。换句话说,就是IBaseListView接口规定了BaseListFragment需要实现哪些主要的功能(爸爸要你干啥,你就得乖乖干啥)。

注释已经写得很清楚了,前四个方法在BaseListFragment中处理,因为所有的加载动作都是一样的。后两个方法交给BaseListFragment的子类去实现,因为每个子类设置的presenter和adapter是不同的。

package com.example.think.base;

import java.util.List;

/**
 * Author: Funny
 * Time: 2018/8/27
 * Description: This is IBaseListView,列表Fragment中该有的行为
 */
public interface IBaseListView extends IBaseView {

    ///////////////////////////////////////////////////////////////////////////
    // 这四个方法在BaseListFragment中处理,因为所有的加载动作都是一样的
    ///////////////////////////////////////////////////////////////////////////
    /**
     * 显示加载动画
     */
    void onShowLoading();

    /**
     * 隐藏加载动画
     */
    void onHideLoading();

    /**
     * 显示网络错误
     */
    void onShowNetError();

    /**
     * 加载完毕
     */
    void onShowNoMore();


    ///////////////////////////////////////////////////////////////////////////
    // 这两个方法交给BaseListFragment的子类去实现,因为每个子类设置的presenter和adapter是不同的
    ///////////////////////////////////////////////////////////////////////////
    /**
     * 设置 presenter
     */
    void setPresenter(T presenter);

    /**
     * 设置适配器
     */
    void onSetAdapter(List list);

}

MVP模式最主要的几个基类就是这些了。基类的好坏与否,直接决定项目结构的优劣。但是抽取基类往往是最难的部分,因为一开始你并不会对之后具体的业务逻辑那么了如指掌,所以你不能清楚的知道在基类的接口中写什么功能方法,哪些方法可以统一处理,哪些方法需要交给具体的子类单独处理......

我们能做的是,对照UI图,多看看接口文档,脑子里有一个成型的界面和界面的功能逻辑。尽量在写代码前,理清楚整个功能的思路,这非常重要。

或者有时候,当你写了一部分代码时,回过头来看,突然发现某一块代码可以抽取和优化,某一个功能可以抽取一个公共的接口方法放到基类里......

具体功能和界面

基类已经简单介绍过了,接着该实现具体的功能页面。

News

News

MD讲了这么多繁琐的基类,写了那么多代码,在手机上跑起来还是一片空白,真TM不爽。终于可以开始撸界面和功能了,不管那么多了,先撸一个RecyclerView列表把场面撑起来再说......

等等,在开始写功能代码前,我们还是需要再理一理这个界面和功能的具体实现思路,抽象成一个接口,把功能方法写在里面。先有优秀的爸爸,才能有更优秀的儿子。

IVideoContract契约类

这里使用的Google官方MVP的结构思想,先创建一个契约类IVideoContract,这个就是MVP的接口类,感觉就像是Model、View、Presenter三者签订某种契约,规定各自需要做的事情。里面写的都是抽象的功能方法,需要子类去具体实现。

契约类规定说:

Model长得太苦逼,性格又内向,网络请求和数据处理的重任就交给你了。

Presenter巧舌如簧,为人处世机灵圆滑,负责沟通和交互的工作。

View天生丽质,颜值高,你就是我们的形象代言人了。

package com.example.think.ui.video;

import com.example.think.base.IBaseListView;
import com.example.think.base.IBasePresenter;
import com.example.think.bean.news.MultiNewsArticleDataBean;
import com.example.think.net.NetCallBack;

import java.util.List;

/**
 * Author: Funny
 * Description: This is MVP契约类
 */
public interface IVideoContract {

    interface View extends IBaseListView {

        /**
         * 请求数据
         */
        void onLoadData();

    }

    interface Presenter extends IBasePresenter {

        /**
         * 请求数据
         */
        void doLoadData(String categoryId);

        /**
         * 设置适配器
         */
        void doSetAdapter(List datas);

        /**
         * 加载更多
         */
        void doLoadMoreData();

        /**
         * 加载完成
         */
        void doShowNoMore();

    }

    interface Model {

        /**
         * 网络请求
         */
        void loadNetData(String category, String time, NetCallBack netCallBack);

    }

}

VideoArticleModel

Model的功能比较单一而繁重,负责做网络请求和数据处理。Model说谁叫自己长得丑,这种脏活累活体力活只能自己去做了。甚至于Model都要开启子线程来工作,不能挡了主线程的路(突然想到身为程序猿的自己不正是扮演着这么苦逼的角色么......)。

package com.example.think.ui.video;

import android.annotation.SuppressLint;

import com.example.think.bean.news.MultiNewsArticleBean;
import com.example.think.bean.news.MultiNewsArticleDataBean;
import com.example.think.net.IMobileVideoApi;
import com.example.think.net.NetCallBack;
import com.example.think.net.RetrofitFactory;
import com.google.gson.Gson;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.android.ActivityEvent;

import java.util.ArrayList;
import java.util.List;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;

/**
 * Author: Funny
 * Description: This is VideoArticleModel网络请求
 */
public class VideoArticleModel implements IVideoContract.Model {

    @SuppressLint("CheckResult")
    @Override
    public void loadNetData(LifecycleProvider provider, String category, String time, NetCallBack netCallBack) {
        Observable observable = RetrofitFactory.getInstance().create(IMobileVideoApi.class).getVideoArticle(category, time);
        observable.subscribeOn(Schedulers.io())
                .map(new Function>() {
                    @Override
                    public List apply(MultiNewsArticleBean multiNewsArticleBean) throws Exception {
                        List dataList = new ArrayList<>();
                        List data = multiNewsArticleBean.getData();
                        for (MultiNewsArticleBean.DataBean datum : data) {
                            String content = datum.getContent();
                            dataList.add(new Gson().fromJson(content, MultiNewsArticleDataBean.class));
                        }
                        return dataList;
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .compose(provider.bindToLifecycle())
                .subscribe(new Consumer>() {
                    @Override
                    public void accept(List multiNewsArticleDataBeans) throws Exception {
                        netCallBack.success(multiNewsArticleDataBeans);
                    }
                }, new Consumer() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        netCallBack.fail(throwable);
                    }
                });


    }
}

VideoArticleFragment

VideoArticleFragment对应MVP的View层,重点来关注一下重写的方法,来理一下它的执行顺序。顺序一定要对,不然可能会报空指针异常或者崩溃。

  1. onSetPresenter()方法是最先执行的,在BaseFragment中的onCreate()方法中执行。

  2. initData()、initView()、包括initEvent()方法是在onCreateView()方法中执行。

  3. fetchData()在onActivityCreated()方法中执行。

这几个方法是在生命周期方法里执行的,不需要我们手动调用。而在契约类IVideoContract中的onLoadData()方法,是需要手动调用,才会执行,所以需要搞清楚。(前面提到的,我们通过方法名前面一个on,就可以知道他是契约类IVideoContract中View层的方法。)

package com.example.think.ui.video;

import android.os.Bundle;
import android.view.View;

import com.example.think.base.BaseListFragment;
import com.example.think.bean.news.MultiNewsArticleDataBean;
import com.example.think.viewHolder.news.NewsArticleVideoViewBinder;

import java.util.List;

import me.drakeet.multitype.Items;
import me.drakeet.multitype.MultiTypeAdapter;

/**
 * Author: Funny
 * Description: This is VideoArticleFragment
 */
public class VideoArticleFragment extends BaseListFragment implements IVideoContract.View {

    private String mCategoryId;
    private Items mDatas = new Items();
    private MultiTypeAdapter mAdapter;


    public static VideoArticleFragment newInstance(String categoryId) {

        Bundle args = new Bundle();
        args.putString("categoryId", categoryId);
        VideoArticleFragment fragment = new VideoArticleFragment();
        fragment.setArguments(args);
        return fragment;
    }

    /**
     * 在BaseFragment中的onCreate方法中执行
     *
     * @param presenter
     */
    @Override
    public void onSetPresenter(IVideoContract.Presenter presenter) {
        if (mPresenter == null) {
            mPresenter = new VideoArticlePresenter(this);
        }
    }

    /**
     * 在onCreateView方法中执行
     */
    @Override
    protected void initData() {
        mCategoryId = getArguments().getString("categoryId");
    }

    @Override
    protected void initView(View view) {
        super.initView(view);
        mAdapter = new MultiTypeAdapter(mDatas);
        mAdapter.register(MultiNewsArticleDataBean.class, new NewsArticleVideoViewBinder());
        mRecyclerView.setAdapter(mAdapter);
    }

    /**
     * 在onActivityCreated方法中执行
     */
    @Override
    public void fetchData() {
        onLoadData();
    }


    @Override
    public void onLoadData() {
        onShowLoading();
        mPresenter.doLoadData(mCategoryId);
    }


    @Override
    public void onSetAdapter(List list) {
        mDatas.clear();
        mDatas.addAll(list);
        mAdapter.notifyDataSetChanged();
        canLoadMore = true;
        mRecyclerView.stopScroll();
    }

}

VideoArticlePresenter

VideoArticlePresenter是负责Model和View的交互。在它的构造方法中,持有一个View的引用,同时还新创建了一个Model对象。有了这两个对象,让View(颜值担当)和Model(程序猿)发生一些关系,不是轻而易举的事情吗。

感觉这个交互很丝滑。

package com.example.think.ui.video;

import android.annotation.SuppressLint;
import android.text.TextUtils;

import com.example.think.bean.news.MultiNewsArticleDataBean;
import com.example.think.net.NetCallBack;
import com.example.think.utils.TimeUtil;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.android.ActivityEvent;

import java.util.ArrayList;
import java.util.List;

import io.reactivex.Observable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Predicate;

/**
 * Author: Funny
 * Description: This is VideoArticlePresenter
 */
public class VideoArticlePresenter implements IVideoContract.Presenter {

    private VideoArticleModel mModel;
    private VideoArticleFragment mView;
    private String mTime;
    private List mDatas;
    private String mCategory;
    private final LifecycleProvider mProvider;

    public VideoArticlePresenter(VideoArticleFragment view) {
        mView = view;
        mModel = new VideoArticleModel();
        mTime = TimeUtil.getCurrentTimeStamp();
        mDatas = new ArrayList<>();
        mProvider = mView.autoRxLifeCycle();
    }

    @Override
    public void doLoadData(String categoryId) {
        mCategory = categoryId;
        mModel.loadNetData(mProvider,categoryId, mTime, new NetCallBack() {
            @SuppressLint("CheckResult")
            @Override
            public void success(List datas) {
                Observable.fromIterable(datas)
                        .filter(new Predicate() {
                            @Override
                            public boolean test(MultiNewsArticleDataBean multiNewsArticleDataBean) throws Exception {
                                //这个时间参数用于加载更多
                                mTime = multiNewsArticleDataBean.getBehot_time();
                                String source = multiNewsArticleDataBean.getSource();
                                if (TextUtils.isEmpty(source)) {
                                    return false;
                                }

                                if (source.contains("头条") || source.contains("问答") || multiNewsArticleDataBean.getTag().contains("ad")) {
                                    return false;
                                }

                                //去除标题重复的新闻
                                for (MultiNewsArticleDataBean data : mDatas) {
                                    if (data.getTitle().equals(multiNewsArticleDataBean.getTitle())) {
                                        return false;
                                    }
                                }

                                return true;
                            }
                        })
                        .toList()
                        .compose(mProvider.bindToLifecycle())
                        .subscribe(new Consumer>() {
                            @Override
                            public void accept(List dataBeans) throws Exception {
                                if (dataBeans != null && dataBeans.size() > 0) {
                                    doSetAdapter(dataBeans);
                                } else {
                                    doShowNoMore();
                                }
                            }
                        });
            }

            @Override
            public void fail(Throwable throwable) {
                doShowNetError();
            }
        });
    }

    @Override
    public void doSetAdapter(List datas) {
        mDatas.addAll(datas);
        mView.onHideLoading();
        mView.onSetAdapter(mDatas);
    }

    @Override
    public void doLoadMoreData() {
        //加载更多数据和category无关,只和当前时间有关,加载更多时,时间参数要用数据里的behot_time
        doLoadData(mCategory);
    }

    @Override
    public void doShowNoMore() {
        mView.onHideLoading();
        mView.onShowNoMore();
    }

    @Override
    public void doRefresh() {
        mDatas.clear();
        mView.onShowLoading();
        mTime = TimeUtil.getCurrentTimeStamp();
        doLoadData(mCategory);
    }

    @Override
    public void doShowNetError() {
        mView.onHideLoading();
        mView.onShowNetError();
    }
}

最后附上github地址,上面的代码都在里面。

https://github.com/FunnyLee/News

这个项目采用谷歌官方MVP架构实现,使用Android主流技术(Retrofit、RxJava、Material Design等等),并且我会持续更新......

你可能感兴趣的:(Google官方Mvp架构详解(基于仿今日头条News项目))