如何使用RxJava实现RecyclerView的无限滚动功能?
解决Android中用RxJava实现RecyclerView无限滚动的问题
Hey,我来帮你搞定这个无限滚动的问题!首先得明确:你现在的代码是一次性拉取所有数据,没有分页逻辑,而且take算子本身是用来限制事件数量的,并不适合处理滚动触发的分页请求。咱们需要把滚动到底部的事件和分页API请求用RxJava的流串联起来,一步步实现需求。
第一步:给API请求加上分页参数
首先得让你的SearchLoansInput支持分页,这样每次请求都能指定拉取第几页、每页多少条数据:
public class SearchLoansInput { // 原有参数 private String userName; // 新增分页参数 private int page; private int pageSize; // Getter & Setter public void setPage(int page) { this.page = page; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } }
对应的Presenter里的请求方法,也要改成接收分页参数,去请求对应页的数据。
第二步:用RxJava构建分页流逻辑
我们用PublishSubject来发送「加载下一页」的事件,然后把这个事件流和API请求流结合,同时处理加载状态、防止重复请求:
先在Activity里添加核心成员变量
public class SalesDashboardActivity2 extends BaseActivity implements SalesDashboardView { private List<DashboardStatusBean> inProgressList = new ArrayList<>(); // 分页控制参数 private int currentPage = 1; private final int PAGE_SIZE = 10; private boolean isLoading = false; // 标记是否正在加载,防止重复请求 private boolean hasMoreData = true; // 标记是否还有更多数据可加载 // 用来发送加载下一页的事件流 private PublishSubject<Integer> loadNextPageSubject = PublishSubject.create(); // 订阅的Disposable,用于页面销毁时取消订阅,避免内存泄漏 private Disposable loadDisposable; // ... 你的原有代码 }
初始化RxJava分页流
在onCreate里初始化这个流,同时触发首次加载:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sales_dashboard2); ButterKnife.bind(this); setSupportActionBar(mToolbar); mSalesDashboardPresenter.attachView(this); // 初始化分页流逻辑 initPagingFlow(); // 首次加载第一页数据 loadNextPageSubject.onNext(currentPage); // 设置RecyclerView滚动监听 setupRecyclerViewScrollListener(); } private void initPagingFlow() { loadDisposable = loadNextPageSubject // 过滤无效请求:正在加载时/没有更多数据时,不处理新的加载事件 .filter(page -> !isLoading && hasMoreData) .doOnNext(page -> isLoading = true) // 标记开始加载 .flatMap(page -> { // 构建分页请求参数 SearchLoansInput input = new SearchLoansInput(); input.setUserName(sessionManager.getMobileNumber()); input.setPage(page); input.setPageSize(PAGE_SIZE); // 调用Presenter的请求,这里建议把Presenter的回调改成返回Observable return mSalesDashboardPresenter.getSalesInquiryObservable(input) .onErrorReturn(throwable -> { // 处理请求错误 runOnUiThread(() -> displayToastMessage("加载失败,请重试")); isLoading = false; return null; }); }) .filter(output -> output != null && output.isSuccess()) .subscribe(output -> { // 解析新数据 List<DashboardStatusBean> newPageData = parseDashboardData(output); // 累加数据(不是替换原有列表!) inProgressList.addAll(newPageData); // 更新RecyclerView mAdapter.notifyDataSetChanged(); // 判断是否还有更多数据:如果返回的数据量小于每页大小,说明已加载完所有数据 hasMoreData = newPageData.size() == PAGE_SIZE; // 标记加载完成 isLoading = false; // 更新当前页码 currentPage++; }); } // 把你原来的数据解析逻辑提取成单独方法,更清晰 private List<DashboardStatusBean> parseDashboardData(SearchLoansOutput searchLoansOutputData) { List<DashboardStatusBean> newList = new ArrayList<>(); SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy"); for (LoanDetail loanDetail : searchLoansOutputData.getLoanDetailList()) { DashboardStatusBean dashboardStatusBean = new DashboardStatusBean(); // 日期格式化 String createdDate = loanDetail.getCreatedDate() != null ? sdf.format(loanDetail.getCreatedDate()) : null; String approveDate = loanDetail.getApprovedDate() != null ? sdf.format(loanDetail.getApprovedDate()) : null; // 赋值其他字段(和你原来的代码一致) dashboardStatusBean.setCreatedDate(createdDate); dashboardStatusBean.setApproveDate(approveDate); dashboardStatusBean.setLoanNumber(loanDetail.getLoanNumber()); dashboardStatusBean.setFirstName(loanDetail.getFirstName()); dashboardStatusBean.setLastName(loanDetail.getLastName()); dashboardStatusBean.setMobileNumber(loanDetail.getMobileNumber()); dashboardStatusBean.setStatus(loanDetail.getStatus().getDisplayName()); dashboardStatusBean.setLoanAmount(String.valueOf(loanDetail.getLoanAmount().setScale(0, BigDecimal.ROUND_UP))); dashboardStatusBean.setLoanAppId(loanDetail.getId()); dashboardStatusBean.setProductAmount(loanDetail.getTotalProductAmount()); dashboardStatusBean.setDownPayment(loanDetail.getDownPayment()); dashboardStatusBean.setProcessingFee(loanDetail.getProcessingFee()); dashboardStatusBean.setTenure(loanDetail.getDesiredItemPaybackTenure()); dashboardStatusBean.setSourceAcquisition(loanDetail.getSourceAcquisition()); dashboardStatusBean.setDocumentUpdatedList(loanDetail.getDocOutputList()); // 筛选符合条件的数据加入列表 String status = loanDetail.getStatus().getDisplayName(); if (status.equals(LoanApplicationStatus.STILL_FILLING.getDisplayName()) || status.equals(LoanApplicationStatus.ONBOARDIN_IN_PROG.getDisplayName()) || status.equals(LoanApplicationStatus.INITIATED.getDisplayName()) || status.equals(LoanApplicationStatus.ADDITIONAL_DATA_REQ.getDisplayName()) || status.equals(LoanApplicationStatus.UPLOAD_IN_PROG.getDisplayName()) || status.equals(LoanApplicationStatus.VERIFIED.getDisplayName()) || status.equals(LoanApplicationStatus.LEAD.getDisplayName())) { newList.add(dashboardStatusBean); } } Util.SALES_IN_PROGRESS_LIST = newList; // 建议尽量避免使用静态变量,直接用inProgressList即可 return newList; }
设置RecyclerView滚动监听,触发加载事件
当用户滚动到接近底部时,发送加载下一页的事件:
private void setupRecyclerViewScrollListener() { mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); // 只处理向下滚动的情况 if (dy <= 0) return; LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); if (layoutManager == null) return; // 获取当前可见的最后一个item位置 int lastVisibleItemPos = layoutManager.findLastVisibleItemPosition(); // 获取总item数 int totalItemCount = layoutManager.getItemCount(); // 当滚动到倒数第2个item时,触发加载下一页(留出提前加载的缓冲空间) if (lastVisibleItemPos >= totalItemCount - 2 && !isLoading && hasMoreData) { loadNextPageSubject.onNext(currentPage); } } }); }
页面销毁时取消订阅,避免内存泄漏
@Override protected void onDestroy() { super.onDestroy(); if (loadDisposable != null && !loadDisposable.isDisposed()) { loadDisposable.dispose(); } mSalesDashboardPresenter.detachView(); }
第三步:改造Presenter,返回Observable(可选但推荐)
如果你的Presenter原来用的是回调模式,建议改成返回Observable,这样更符合RxJava的链式调用风格:
public class SalesDashboardPresenter { private SalesApiService apiService; public Observable<SearchLoansOutput> getSalesInquiryObservable(SearchLoansInput input) { return apiService.searchLoans(input) .subscribeOn(Schedulers.io()) // 在IO线程执行网络请求 .observeOn(AndroidSchedulers.mainThread()); // 在主线程处理结果 } }
为什么原来的take算子没用?
take(n)是用来截取流中的前n个事件,而我们的场景是每次滚动到底部就触发一个请求,是一个持续的事件流,直到没有更多数据为止。所以需要用filter控制请求触发条件,用flatMap把页码事件转换成API请求流,而不是用take。
另外还要注意几个核心点:
- 防止重复请求:用
isLoading标记,正在加载时不处理新的滚动事件 - 判断是否还有更多数据:如果返回的数据量小于每页大小,说明已加载完所有数据
- 内存泄漏:页面销毁时一定要取消订阅Disposable
内容的提问来源于stack exchange,提问作者Maulik Dodia
相关产品推荐
相关产品推荐

