Android Paging库使用详解(小结)
Android分页包能够更轻易地在RecyclerView里面缓慢且优雅地加载数据.
许多应用从数据源消耗数据,数据源里面有大量的数据,但是一次却只展示一小部分.
分页包帮助应用观测和展示大量数据的合理数目的子集.这个功能有如下几个优势:
- 数据请求消耗更少的网络带宽和系统资源.
- 即使在数据更新期间,应用依然对用户输入响应迅速.
添加分页依赖
按照如下代码添加依赖:
dependencies{ defpaging_version="1.0.0" implementation"android.arch.paging:runtime:$paging_version" //alternatively-withoutAndroiddependenciesfortesting testImplementation"android.arch.paging:common:$paging_version" //optional-RxJavasupport,currentlyinreleasecandidate implementation"android.arch.paging:rxjava2:1.0.0-rc1" }
备注:分页包帮助开发者在UI的列表容器中顺畅地展示数据,而不管是使用设备内部的数据库还是从应用后端拉取数据.
库架构
分页库的核心构件是PagedList类,它是一个集合,用于异步加载应用数据块或者数据页.该类在应用的其它架构之间充当中介.
Data
每一个PagedList实例从DataSource中加载最新的应用数据.数据从应用后端或者数据库流入PagedList对象.分页包支持多样的应用架构,包括脱机数据库和与后台服务器通讯的数据库.
UI
PagedList类通过PagedListAdapter加载数据项到RecyclerView里面.在加载数据的时候,这些类协同工作,拉取数据并展示内容,包括预取看不见的内容并在内容改变时加载动画.
支持不同的数据架构
分页包支持应用架构,包括应用拉取数据的地方是从后台服务器,还是本机数据库,还是两者的结合.
只有网络
要展示后台数据,需要使用Retrofit的同步版本,加载信息到自定义的DataSource对象中.
备注:分页包的DataSource对象并没有提供任何错误处理机制,因为不同的应用需要用不同的方式处理和展示UI错误.如果错误发生了,顺从结果的回调,然后稍后重试.
只有数据库
要设置RecyclerView观测本地存储,偏向于使用Room持久化库.用这种方式,无论任何时候数据库数据插入或者修改,这些改变会自动地在负责展示这些数据的RecyclerView展示出来.
网络+数据库
在开始观测数据库之后,你能够通过使用PagedList.BoundaryCallback来监听数据库什么时候过期.之后,你可能从网络拉取更多的数据,并把它们插入到数据库中.如果UI正在展示数据库,以上就是你所需要做的全部.
下面的代码片断展示了BoundaryCallback的使用实例:
classConcertViewModel{ funsearch(query:String):ConcertSearchResult{ valboundaryCallback= ConcertBoundaryCallback(query,myService,myCache) //Error-handlingnotshowninthissnippet. valnetworkErrors=boundaryCallback.networkErrors } } classConcertBoundaryCallback( privatevalquery:String, privatevalservice:MyService, privatevalcache:MyLocalCache ):PagedList.BoundaryCallback(){ overridefunonZeroItemsLoaded(){ requestAndSaveData(query) } overridefunonItemAtEndLoaded(itemAtEnd:Concert){ requestAndSaveData(query) } }
处理网络错误
在使用网络拉取或者分页的数据,而这些数据正在使用分页包展示的时候,不总是把网络分为要么"可用"要么"不可能"是很重要的,因为许多连接是间歇性或者成片的:
- 特定的服务器可能不能响应网络请求;
- 设备可能联接了慢的或者弱的网络;
应用应该检查每一个请求是否成功,并且在网络不可用的情形下,尽可能快地恢复.比如,你可以为用户提供一个"重试"按钮,如果数据没有刷新成功的话.如果在数据分页期间发生错误,最好自动地重新分页请求.
更新已有应用
如果应用已经从网络或者数据库消费数据,很大可能可以直接升级到分页库提供的功能.
自定义分页解决方案
如果你使用了自定义功能加载数据源中的小的数据集,你可以使用PagedList类取代这个逻辑.PagedList类实例提供了内建的连接,到通用的数据源.这些实例也提供了在应用中引用的RecyclerView的适配器.
使用列表而非分页加载的数据
如果你使用内存里的列表作为UI适配器的后备数据结构,考虑使用PagedList类观测数据更新,如果列表中数据项变得很多的话.PagedList实例既可以使用LiveData对UI传递数据更新,同时最小化了加载时间和内存使用.然而,应用中使用PagedList对象代替List并不要求对UI结构和数据更新逻辑作任何改变.
使用CursorAdapter将数据cursor与列表视图联系起来
应用也许会使用CursorAdapter将数据从Cursor跟ListView连接起来.在这种情况下,通常需要从ListView迁移到RecyclerView,然后使用Room或者PositionalDataSource构件代替Cursor,当然,这主要依据于Cursor实例能否访问SQLite数据库.
在一些情况下,比如使用Spinner实例的时候,你仅仅提供了Adapter本身.然后一个库使用了加载进adapter中的数据,并展示了数据.在这些情况下,把adapter数据类型转化为LiveData
使用AsyncListUtil异步加载内容
如果你在使用AsyncListUtil对象异步地加载和展示分组信息的话,分页包将会使得加载数据更加方便:
- 数据并不需要定位.分页包让你直接从后台使用网络提供的键加载数据.
- 数据量太大.使用分页包可以将数据加载分页直到没有任何数据留下.
- 更方便地观测数据.分页包能够展示应用在可观测数据结构中持有的ViewModel.
数据库例子
使用LiveData观测分页数据
下面的示例代码展示了所有一起工作的碎片.当演唱会事件在数据库中添加,删除或者修改的修改的时候,RecyclerView中的内容自动且高效地更新:
@Dao interfaceConcertDao{ //TheIntegertypeparametertellsRoomtouseaPositionalDataSource //object,withposition-basedloadingunderthehood. @Query("SELECT*FROMuserORDERBYconcertDESC") funconcertsByDate():DataSource.Factory} classMyViewModel(concertDao:ConcertDao):ViewModel(){ valconcertList:LiveData >=LivePagedListBuilder( concertDao.concertsByDate(), /*pagesize*/20 ).build() } classMyActivity:AppCompatActivity(){ publicoverridefunonCreate(savedState:Bundle?){ super.onCreate(savedState) valviewModel=ViewModelProviders.of(this) .get(MyViewModel::class.java!!) valrecyclerView=findViewById(R.id.concert_list) valadapter=ConcertAdapter() viewModel.concertList.observe(this,{pagedList-> adapter.submitList(pagedList)}) recyclerView.setAdapter(adapter) } } classConcertAdapter(): PagedListAdapter (DIFF_CALLBACK){ funonBindViewHolder(holder:ConcertViewHolder,position:Int){ valconcert=getItem(position) if(concert!=null){ holder.bindTo(concert) }else{ //Nulldefinesaplaceholderitem-PagedListAdapterautomatically //invalidatesthisrowwhentheactualobjectisloadedfromthe //database. holder.clear() } } companionobject{ privatevalDIFF_CALLBACK=object:DiffUtil.ItemCallback (){ //Concertdetailsmayhavechangedifreloadedfromthedatabase, //butIDisfixed. overridefunareItemsTheSame(oldConcert:Concert, newConcert:Concert):Boolean= oldConcert.id==newConcert.id overridefunareContentsTheSame(oldConcert:Concert, newConcert:Concert):Boolean= oldConcert==newConcert } } }
使用RxJava2观测分页数据
如果你偏爱使用RxJava2而非LiveData,那么你可以创建Observable或者Flowable对象:
classMyViewModel(concertDao:ConcertDao):ViewModel(){ valconcertList:Flowable>=RxPagedListBuilder( concertDao.concertsByDate(), /*pagesize*/50 ).buildFlowable(BackpressureStrategy.LATEST) }
之后你可以按照如下代码开始和停止观测数据:
classMyActivity:AppCompatActivity(){ privatelateinitvaradapter:ConcertAdapterprivatelateinitvarviewModel:MyViewModel privatevaldisposable=CompositeDisposable() publicoverridefunonCreate(savedState:Bundle?){ super.onCreate(savedState) valrecyclerView=findViewById(R.id.concert_list) viewModel=ViewModelProviders.of(this).get(MyViewModel::class.java!!) adapter=ConcertAdapter() recyclerView.setAdapter(adapter) } overridefunonStart(){ super.onStart() disposable.add(viewModel.concertList.subscribe({ flowableList->adapter.submitList(flowableList) })) } overridefunonStop(){ super.onStop() disposable.clear() } }
基于RxJava2解决方案的ConcertDao和ConcertAdapter代码,和基于LiveData解决方案的代码是一样的.
UI构件及其出发点
将UI和视图模型联接起来
你可以按照如下方式,将LiveData
privatevaladapter=ConcertPagedListAdapter() privatelateinitvarviewModel:ConcertViewModel overridefunonCreate(savedInstanceState:Bundle?){ viewModel=ViewModelProviders.of(this) .get(ConcertViewModel::class.java) viewModel.concerts.observe(this,adapter::submitList) }
当数据源提供一个新PagedList实例的时候,activity会将这些对象改善给adapter.PagedListAdapter实现,定义了更新如何计算,自动地处理分页和列表不同.由此,你的ViewHolder只需要绑定到特定的提供项:
classConcertPagedListAdapter():PagedListAdapter( object:DiffUtil.ItemCallback (){ //TheIDpropertyidentifieswhenitemsarethesame. overridefunareItemsTheSame(oldItem:Concert,newItem:Concert) =oldItem.id=newItem.id //Usethe"=="operator(orObject.equals()inJava-basedcode)toknow //whenanitem'scontentchanges.Implementequals(),orwritecustom //datacomparisonlogichere. overridefunareContentsTheSame(oldItem:Concert,newItem:Concert)= oldItem.name==newItem.name&&oldItem.date==newItem.date } ){ overridefunonBindViewHolder(holder:ConcertViewHolder,position:Int){ valconcert:Concert?=getItem(position) //Notethat"concert"isaplaceholderifit'snull holder.bind(concert) } }
PagedListAdapter使用PagedList.Callback对象处理分页加载事件.当用户滑动时,PagedListAdapter调用PagedList.loadAround()方法将从DataSource中拉聚拢数据项提示提供给基本的PagedList.
备注:PageList是内容不可变的.这意味着,尽管新内容能够被加载到PagedList实例中,但已加载项一旦加载完成便不能发生改变.由此,如果PagedList中的内容发生改变,PagedListAdapter对象将会接收到一个包含已更新信息的全新的PagedList.
实现diffing回调
先前的代码展示了areContentsTheSame()的手动实现,它比较了对象的相关的域.你也可以使用Java中的Object.equals()方法或者Kotlin中的==操作符.但是要确保要么实现了对象中的equals()方法或者使用了kotlin中的数据对象.
使用不同的adapter类型进行diffing
如果你选择不从PagedListAdapter继承--比如你在使用一个提供了自己的adapter的库的时候--你依然可以通过直接使用AsyncPagedListDiffer对象使用分页包adapter的diffing功能.
在UI中提供占位符
在应用完成拉取数据之前,如果你想UI展示一个列表,你可以向用户展示占位符列表项.RecyclerView通过将列表项临时地设置为null来处理这个情况.
备注:默认情况下,分页包开启了占位符行为.
占位符有如下好处:
- 支持scrollbar.PagedList向PagedListAdapter提供了大量的列表项.这个信息允许adapter绘制一个表示列表已满的scrollbar.当新的页加载时,scrollbar并不会跳动,因为列表是并不没有改变它的size.
- 不需要"正在加载"旋转指针.因为列表大小已知,没必要提醒用户有更多的数据项正在加载.占位符本身表达了这个信息.
在添加占位符的支持之前,请牢记以下先置条件:
- 要求集合中数据可数.来自Room持久化库的DataSource实例能够高效地计算数据项.然而,如果你在用自定义本地存储方案或者只有网络的数据架构,想了解数据集中有多少数据项可能代价很高,甚至不可能.
- 要求adapter负责未加载数据项.你正在使用的adapter或者展示机制来准备填充列表,需要处理null列表项.比如,当将数据绑定到ViewHolder的时候,你需要提供默认值表示未加载数据.
- 要求数据相同数量的itemview.如果列表项数目能够基于内容发生改变,比如,社交网络更新,交叉淡入淡出看起来并不好.在这种情况下,强烈推荐禁掉占位符.
数据构件及其出发点
构建可观测列表
通常情况下,UI代码观测LiveData
要创建这么一个可观测PagedList对象,需要将DataSource.Factory实例传给LivePageListBuilder/RxPagedListBuilder对象.一个DataSource对象对单个PagedList加载分页.这个工厂类为内容更新创建PagedList实例,比如数据库表验证,网络刷新等.Room持久化库能够提供DataSource.Factory,或者自定义.
如下代码展示了如何在应用的ViewModel类中使用Room的DataSource.Factory构建能力创建新的LiveData
ConcertDao.kt:
interfaceConcertDao{ //TheIntegertypeparametertellsRoomtouseaPositionalDataSource //object,withposition-basedloadingunderthehood. @Query("SELECT*FROMconcertsORDERBYdateDESC") publicabstractDataSource.FactoryconcertsByDate() }
ConcertViewModel.kt:
//TheIntegertypeargumentcorrespondstoaPositionalDataSourceobject. valmyConcertDataSource:DataSource.Factory= concertDao.concertsByDate() valmyPagedList=LivePagedListBuilder(myConcertDataSource,/*pagesize*/20) .build()
定义分页配置
要想为复杂情形更深入地配置LiveData
- 页大小:每一页的数据量.
- 预取距离:给定UI中最后可见项,超过该项之后多少项,分页包要尝试提前提取数据.这个值应该比pagesize大几倍.
- 占位符展示:决定了UI是否会为还没有完成加载的数据项展示占位符.
如果你想要对分布包从数据库加载中设置更多的控件,要像下面的代码一样,传递自定义的Executor对象给LivePagedListBuilder:
EventViewModel.kt:
valmyPagingConfig=PagedList.Config.Builder() .setPageSize(50) .setPrefetchDistance(150) .setEnablePlaceholders(true) .build() //TheIntegertypeargumentcorrespondstoaPositionalDataSourceobject. valmyConcertDataSource:DataSource.Factory= concertDao.concertsByDate() valmyPagedList=LivePagedListBuilder(myConcertDataSource,myPagingConfig) .setFetchExecutor(myExecutor) .build()
选择正确的数据源类型
连接更最好地处理源数据结构的数据源很重要:
- 如果加载的页嵌套了之前/之后页的key的话,使用PageKeyDataSource.比如,比如你正在从网络中拉取社交媒体博客,你也许需要传递从一次加载向下一次加载的nextPagetoken.
- 如果需要使用每N项数据项的数据拉取每N+1项的话,使用ItemKeyedDataSource.比如,你在为一个讨论型应用拉取螺纹评论,你可能需要传递最后一条评论的ID来获取下一条评论的内容.
- 如果你需要从数据商店中的任意位置拉取分页数据的话,使用PositionalDataSource.这个类支持请求任意位置开始的数据集.比如,请求也许返回从位置1200开始的20条数据.
通知数据非法
在使用分页包时,在表或者行数据变得陈腐时,取决于数据层来通知应用的其它层.要想这么做的话,需要从DataSource类中调用invalidate()方法.
备注:UI也可以使用"滑动刷新"模式来触发数据非法功能.
构建自己的数据源
如果你使用了自定义的数据解决方案,或者直接从网络加载数据,你可以实现一个DataSource子类.下面的代码展示了数据源从给定的concert起始时间切断:
classConcertTimeDataSource(privatevalconcertStartTime:Date): ItemKeyedDataSource(){ overridefungetKey(item:Concert)=item.startTime overridefunloadInitial( params:LoadInitialParams , callback:LoadInitialCallback ){ valitems=fetchItems(concertStartTime,params.requestedLoadSize) callback.onResult(items) } overridefunloadAfter( params:LoadParams , callback:LoadCallback ){ valitems=fetchItemsAfter( date=params.key, limit=params.requestedLoadSize) callback.onResult(items) } }
通过创建真实的DataSource.Factory子类,你之后能够加载自定义的数据到PagedList对象.下面的代码展示了如何创建在之前代码中定义的自定义数据源:
classConcertTimeDataSourceFactory(privatevalconcertStartTime:Date): DataSource.Factory(){ valsourceLiveData=MutableLiveData () overridefuncreate():DataSource { valsource=ConcertTimeDataSource(concertStartTime) sourceLiveData.postValue(source) returnsource } }
考虑内容更新
当你构建可观测PagedList对象的时候,考虑一下内容是如何更新的.如果你直接从Room数据库中加载数据,更新会自动地推送到UI上面.
如果你在使用分页的网络API,通常你会有用户交互,比如"滑动刷新",把它作为信号去验证当前DataSource非法并请求一个新的.这个行为出行在下面的代码中:
classConcertActivity:AppCompatActivity(){ overridefunonCreate(savedInstanceState:Bundle?){ ... concertViewModel.refreshState.observe(this,Observer{ swipeRefreshLayout.isRefreshing= it==NetworkState.LOADING }) swipeRefreshLayout.setOnRefreshListener{ concertViewModel.invalidateDataSource() } } }
提供数据表现之间的映射
对于DataSource加载的数据,分页包支持基于数据项和基于页的转换.
下面的代码中,concert名和日期的联合被映射成包含姓名和日期的字符串:
classConcertViewModel:ViewModel(){ valconcertDescriptions:LiveData> init{ valfactory=database.allConcertsFactory() .map{concert-> concert.name+"-"+concert.date } concerts=LivePagedListBuilder(factory,30).build() } } }
如果在数据加载之后,想要包裹,转换或者准备item,这将非常有用.因为这个工作是在获取执行器中完成的,你可以在其中执行花销巨大的工作,比如,从硬盘中读取,查询数据库等.
备注:JOIN查询总是比作为map()一部分的查询要高效.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。