You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无数字ID场景下Android Paging Library适配问题求助

如何在Android Paging Library中处理无数字ID的API分页?

我在调用旅游类API获取国家列表时,遇到了一个分页问题:接口返回的每个条目只有字符串类型的id(比如"id": "Benin"),没有数字ID,这导致我使用Android Paging Library的ItemKeyedDataSource时卡壳了——getKey()方法不知道该返回什么,而像GitHub API那种带数字ID的接口适配起来就很顺畅。

API响应示例

{ 
  "results": [ 
    { 
      "name": "Benin", 
      "country_id": "Benin", 
      "snippet": "A safe and relatively easy country for travellers to visit; birthplace of the Voodoo religion and former home of the Kingdom of Dahomey.", 
      "parent_id": null, 
      "score": 3.98127216481287, 
      "id": "Benin" 
    } 
  ], 
  "estimated_total": 30845, 
  "more": true 
}

问题代码

ItemKeyedCountryDataSource.java

public class ItemKeyedCountryDataSource extends ItemKeyedDataSource<Integer, CountryResult> { 
    private EndpointHelper endpointHelper; 
    ItemKeyedCountryDataSource() { 
        endpointHelper = EndpointHelper.getInstance(); 
    } 

    @Override 
    public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull final LoadInitialCallback<CountryResult> callback) { 
        final List<CountryResult> countryResultList = new ArrayList<>(); 
        endpointHelper.getCountryList(0) 
                .subscribeOn(Schedulers.io()) 
                .observeOn(AndroidSchedulers.mainThread()) 
                .retry(1) 
                .onErrorResumeNext(new Func1<Throwable, Observable<? extends CountryWrapper>>() { 
                    @Override 
                    public Observable<? extends CountryWrapper> call(Throwable throwable) { 
                        return Observable.error(throwable); 
                    } 
                }) 
                .subscribe(new Subscriber<CountryWrapper>() { 
                    @Override 
                    public void onCompleted() { } 
                    @Override 
                    public void onError(Throwable e) { } 
                    @Override 
                    public void onNext(CountryWrapper countryWrapper) { 
                        if (countryWrapper.getResults().size() > 0) { 
                            countryResultList.addAll(countryWrapper.getResults()); 
                            callback.onResult(countryResultList); 
                        } 
                    } 
                }); 
    } 

    @Override 
    public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull final LoadCallback<CountryResult> callback) { 
        final List<CountryResult> countryResultList = new ArrayList<>(); 
        endpointHelper.getCountryList(params.requestedLoadSize) 
                .subscribeOn(Schedulers.io()) 
                .observeOn(AndroidSchedulers.mainThread()) 
                .retry(1) 
                .onErrorResumeNext(new Func1<Throwable, Observable<? extends CountryWrapper>>() { 
                    @Override 
                    public Observable<? extends CountryWrapper> call(Throwable throwable) { 
                        return Observable.error(throwable); 
                    } 
                }) 
                .subscribe(new Subscriber<CountryWrapper>() { 
                    @Override 
                    public void onCompleted() { } 
                    @Override 
                    public void onError(Throwable e) { } 
                    @Override 
                    public void onNext(CountryWrapper countryWrapper) { 
                        if (countryWrapper.getResults().size() > 0) { 
                            countryResultList.addAll(countryWrapper.getResults()); 
                            callback.onResult(countryResultList); 
                        } 
                    } 
                }); 
    } 

    @Override 
    public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<CountryResult> callback) { 
        //Do nothing 
    } 

    @NonNull 
    @Override 
    public Integer getKey(@NonNull CountryResult item) { 
        return ?; // what do I return here? 
    } 
}

ViewModel.java

public class SearchableActivityViewModel extends ViewModel { 
    private LiveData<PagedList<CountryResult>> countryResult; 
    SearchableActivityViewModel() { 
        Executor executor = Executors.newFixedThreadPool(5); 
        CountryResultDataSourceFactory countryResultDataSourceFactory = new CountryResultDataSourceFactory(); 
        PagedList.Config config = new PagedList.Config.Builder() 
                .setEnablePlaceholders(false) 
                .setInitialLoadSizeHint(20) //first load 
                .setPageSize(21) 
                .build(); 
        //noinspection unchecked 
        countryResult = new LivePagedListBuilder<String, CountryResult>(countryResultDataSourceFactory, config) 
                .setFetchExecutor(executor) 
                .build(); 
    } 

    public LiveData<PagedList<CountryResult>> getCountryResult(){ 
        return countryResult; 
    } 

    @Override 
    protected void onCleared() { 
        super.onCleared(); 
    } 
}

解决方案

针对这个问题,我们有两种处理思路,取决于你的API支持哪种分页方式:

方案一:如果API支持基于字符串ID的分页(比如after参数)

这种情况下,我们可以继续使用ItemKeyedDataSource,只需要把泛型类型从Integer改成String即可:

  1. 修改数据源泛型
    把ItemKeyedDataSource<Integer, CountryResult>改为ItemKeyedDataSource<String, CountryResult>。

  2. 实现getKey()方法
    直接返回条目的字符串ID:

    @NonNull
    @Override
    public String getKey(@NonNull CountryResult item) {
        return item.getId(); // 返回字符串类型的ID作为分页键
    }
    
  3. 修正分页请求逻辑
    原来的loadAfter方法里你错误地传递了params.requestedLoadSize,实际上应该用params.key(上一页最后一个条目的ID)作为分页参数传给API:

    @Override
    public void loadAfter(@NonNull LoadParams<String> params, @NonNull final LoadCallback<CountryResult> callback) {
        final List<CountryResult> countryResultList = new ArrayList<>();
        // 传递上一页最后一个item的ID,请求下一页数据
        endpointHelper.getCountryListAfter(params.key)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .retry(1)
                .onErrorResumeNext(throwable -> Observable.error(throwable))
                .subscribe(countryWrapper -> {
                    if (!countryWrapper.getResults().isEmpty()) {
                        countryResultList.addAll(countryWrapper.getResults());
                        callback.onResult(countryResultList);
                    }
                });
    }
    

    同时需要在EndpointHelper中新增getCountryListAfter(String lastId)方法,构建符合API要求的分页请求。

方案二:如果API只支持offset/limit分页

如果你的API只能通过offset(偏移量)和limit(每页数量)来分页,那更适合使用PageKeyedDataSource,它基于页的键(比如offset数值)来处理分页:

  1. 创建PageKeyedCountryDataSource

    public class PageKeyedCountryDataSource extends PageKeyedDataSource<Integer, CountryResult> {
        private EndpointHelper endpointHelper;
    
        PageKeyedCountryDataSource() {
            endpointHelper = EndpointHelper.getInstance();
        }
    
        @Override
        public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<Integer, CountryResult> callback) {
            // 初始加载,offset=0
            endpointHelper.getCountryList(0, params.requestedLoadSize)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .retry(1)
                    .subscribe(countryWrapper -> {
                        if (!countryWrapper.getResults().isEmpty()) {
                            // 下一页的offset = 当前offset + 加载数量
                            callback.onResult(countryWrapper.getResults(), null, params.requestedLoadSize);
                        }
                    }, Throwable::printStackTrace);
        }
    
        @Override
        public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, CountryResult> callback) {
            // 不需要向前加载,留空即可
        }
    
        @Override
        public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, CountryResult> callback) {
            // params.key是上一页的offset,计算下一页offset
            int nextOffset = params.key + params.requestedLoadSize;
            endpointHelper.getCountryList(nextOffset, params.requestedLoadSize)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .retry(1)
                    .subscribe(countryWrapper -> {
                        if (!countryWrapper.getResults().isEmpty()) {
                            // 判断是否还有更多数据
                            Boolean hasMore = nextOffset < countryWrapper.getEstimated_total();
                            callback.onResult(countryWrapper.getResults(), hasMore ? nextOffset : null);
                        }
                    }, Throwable::printStackTrace);
        }
    }
    
  2. 更新ViewModel中的数据源工厂
    确保你的CountryResultDataSourceFactory返回PageKeyedCountryDataSource,同时修改LivePagedListBuilder的泛型:

    countryResult = new LivePagedListBuilder<Integer, CountryResult>(countryResultDataSourceFactory, config)
            .setFetchExecutor(executor)
            .build();
    

总结

  • 如果API支持基于条目字符串ID的分页,优先用ItemKeyedDataSource<String, CountryResult>;
  • 如果API只支持offset分页,切换到PageKeyedDataSource<Integer, CountryResult>更合适。

内容的提问来源于stack exchange,提问作者nuhkoca

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:53:43