【Android】组件化打造第三方知乎日报系列(三)——日报列表的开发

本节完整代码可以前往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;
}

查看效果

最后我们可以看一下代码编写完成后的效果:

N0tExpectErr0r

N0tExpectErr0r

一名热爱代码的 Android 开发者

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>