本节完整代码可以前往github查看,项目地址:https://github.com/N0tExpectErr0r/Zhihu-Daily
DailyModule
我们在此处创建了一个DailyModule来编写我们的首页推荐模块。它仍然是组件层的一个Module,因此gradle的编写与之前HomeModule的编写大同小异,下面来讲一下这部分的一些关键点。
下拉加载更多的RecyclerView
在知乎日报的日报列表中,我们的RecyclerView肯定不能像原来一样显示固定的数据了,应该拥有上拉刷新,下拉加载更多的功能。
关于上拉刷新功能,Android已经有了SwipeRefreshLayout,我们不再不需要自己去实现。但下拉加载在Android原生的控件中并没有这个功能。由于功能简单,就不再导第三方库了。
在本项目中,通过重写RecyclerView的OnScrollListener达到了下拉加载更多的功能。代码如下:
public abstract class OnMoreScrollListener extends RecyclerView.OnScrollListener {
private RecyclerView mRecyclerView;
protected OnMoreScrollListener(RecyclerView recyclerView) {
this.mRecyclerView = recyclerView;
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
RecyclerView.LayoutManager manager = mRecyclerView.getLayoutManager();
BaseAdapter adapter = (BaseAdapter) mRecyclerView.getAdapter();
if (null == manager) {
throw new RuntimeException("you should call setLayoutManager() first!!");
}
if (manager instanceof LinearLayoutManager) {
int lastCompletelyVisibleItemPosition = ((LinearLayoutManager) manager)
.findLastCompletelyVisibleItemPosition();
if (lastCompletelyVisibleItemPosition == adapter.getItemCount() - 1 && adapter.hasMore()) {
onLoadMore();
}
}
}
protected abstract void onLoadMore();
}
可以看到,我们在滑动到最后一个Item的时候进行了数据的加载。由于知乎日报api返回的数据是不定的,因此这里的OnScrollListener是根据BaseAdapter是否还有更多进行的判断是否需要加载更多。
如果读者所使用的api是定量的分页加载,可以考虑通过比较adapter的itemCount与api具体返回的数据数量从而判断是否需要加载更多。
另外,我在项目中封装了BaseAdapter及BaseViewHolder以简化Adapter的书写,如果想要了解具体代码可以前往本项目的github查看。
网络请求的处理
关于布局文件个人认为这里不需要讲太多,因此在这里仅仅讲一下网络请求部分的设计。
整体架构
这里采用的是一种变种的MVP架构,具体关系如下图:
我们的网络请求通过OkHttp在Repository中进行,并返回对应的Observable。之后Presenter获取到数据后对数据进行一系列的处理并且通过Callback的方式回调给View。View收到数据后将数据传递给Adapter从而完成界面数据的显示。
而我们都知道,由于View层的生命周期与RxJava的事件的生命周期不一致,在我们使用RxJava的过程中很容易导致内存泄漏。为了解决这个问题,我们引入了AutoDispose库,对它进行了一些封装,从而在Presenter中解决我们的内存泄漏问题。
契约类
由于我们已经有了一个BaseContract类,因此仅仅需要继承它并声明一些业务相关的方法即可。
public interface DailyContract {
interface View extends BaseContract.View{
void onLoadBannerFinish(List<TopStoryBean> topStories);
void onRefreshListFinish(List<StoryBean> stories);
void onLoadMoreListFinish(List<StoryBean> stories);
void onLoadError();
}
abstract class Presenter extends BaseContract.RepoPresenter<View, DailyRepository>{
public Presenter(Context context, View view, DailyRepository repository) {
super(context, view, repository);
}
public abstract void refreshList();
public abstract void loadMoreList();
}
}
Repository
Repository负责了请求的执行,并将结果返回给Presenter类,下面是此处的一部分代码,可以看到此处请求使用OkHttp来执行,其中请求的参数date由外面传递进来,最后返回Observable<StoryData>
。
public Observable<StoryData> loadMoreStory(String date){
String url = DATE_STORIES_URL+date;
return Observable.create(new ObservableOnSubscribe<Response>() {
@Override
public void subscribe(ObservableEmitter<Response> emitter) throws Exception {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.get()
.url(url)
.build();
Call call = client.newCall(request);
emitter.onNext(call.execute());
}
}).map(new Function<Response, StoryData>() {
@Override
public StoryData apply(Response response) throws Exception {
String json = response.body().string();
StoryData data = Gsoner.fromJson(json, StoryData.class);
return data;
}
});
}
Presenter
Presenter负责了对返回的Observable数据的处理,并通知View回调。在Prensenter中使用了一个全局变量Date,它保存的是下一次请求所需要用到的Date,是由知乎日报的接口返回出来的。而View层不需要知道具体请求是怎样进行处理。
在date为空的情况下(按道理不会),则会以当天时间构建一个相同格式的Date字符串获取数据。
可以看到在RxJava调用过程中我们使用了在BaseContract中封装的bindLifecycle
方法来解决RxJava的内存泄漏问题,使得时间与Fragment的生命周期绑定。
private String date;
@Override
public void loadMoreList() {
if(date == null){
Date tmp = new Date();
DateFormat format = new SimpleDateFormat("yyyyMMdd");
date = format.format(tmp);
}
getRepository().loadMoreStory(date)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.as(bindLifecycle())
.subscribe(storyData -> {
date = storyData.getDate();
getView().onLoadMoreListFinish(storyData.getStoryList());
}, throwable -> {
getView().onLoadError();
throwable.printStackTrace();
});
}
View
此处我们使用的是封装好的DailyMvpFragment,并且implements了我们的View接口。关键的代码在于数据的回调部分。
可以看到,我们在onCreatePresenter中创建了Presenter并返回。
之后在数据的回调中,我们主要做的事就是向Adapter及BannerView传递得到的数据。这样就完成了数据加载成功后界面的刷新。
@Override
protected DailyPresenter onCreatePresenter() {
return new DailyPresenter(getContext(),this,new DailyRepository());
}
@Override
public void onLoadBannerFinish(List<TopStoryBean> topStories) {
List<String> imgUrls = new ArrayList<>();
for (TopStoryBean topStory : topStories) {
imgUrls.add(topStory.getImage());
}
if (mBannerView == null){
mBannerView = mAdapter.getBannerView();
}
mBannerView.setImageUrlList(imgUrls);
}
@Override
public void onRefreshListFinish(List<StoryBean> stories) {
mAdapter.setDatas(stories);
mSrlRefresh.setRefreshing(false);
}
@Override
public void onLoadMoreListFinish(List<StoryBean> stories) {
mAdapter.addDatas(stories);
}
@Override
public void onLoadError() {
showToast("网络错误,请检查网络设置 ");
}
通过Router整合到HomeModule中
最后我们别忘记了将这个Fragment整合到HomeModule中。
我们在HomeModule中通过路径用ARouter路由返回了当前的DailyFragment,并添加到FragmentList中。
private List<Fragment> getFragmentList() {
List<Fragment> fragmentList = new ArrayList<>();
Fragment fragment = (Fragment) ARouter.getInstance()
.build(RouterConstant.FRAGMENT_DAILY_LIST)
.navigation();
fragmentList.add(fragment);
return fragmentList;
}
查看效果
最后我们可以看一下代码编写完成后的效果: