先来一发Google官方Mvp架构地址:
https://github.com/googlesamples/android-architecture/tree/todo-mvp/
基类介绍
BaseFragment
这个类是Fragment的一级父类。主要完成以下功能:
在类的声明上添加了一个IBasePresenter的泛型,并且实现了IBaseView接口。
统一封装了EventBus的使用,在基类中注册和反注册EventBus。子类如果需要使用EventBus,则只需要添加相应的注解即可。(关于通过注解的方式在基类中封装EventBus,可以参考https://blog.csdn.net/xieluoxixi/article/details/78262765)
在Fragment挂载在Activity的时候,保存了一个上下文Context对象。
在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图,多看看接口文档,脑子里有一个成型的界面和界面的功能逻辑。尽量在写代码前,理清楚整个功能的思路,这非常重要。
或者有时候,当你写了一部分代码时,回过头来看,突然发现某一块代码可以抽取和优化,某一个功能可以抽取一个公共的接口方法放到基类里......
具体功能和界面
基类已经简单介绍过了,接着该实现具体的功能页面。
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层,重点来关注一下重写的方法,来理一下它的执行顺序。顺序一定要对,不然可能会报空指针异常或者崩溃。
onSetPresenter()方法是最先执行的,在BaseFragment中的onCreate()方法中执行。
initData()、initView()、包括initEvent()方法是在onCreateView()方法中执行。
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等等),并且我会持续更新......