Android Paging Library按页获取网络数据实例

泡在网上的日子 / 文 发表于2017-09-22 11:11 第次阅读 Paging Library

原文:Android Paging Library?—?Make your lists as efficient as possible literally in just an hour directly from the network! ヽ(*?ω?)??

新的?Paging Library?成为了?Architecture Components?的一部分。虽然现在还是alpha阶段,但是无疑你已经开始准备尝试了!我不准备全去讲它的用法,因为本文只是对Chris Craik??这篇文章的补充。

因为官方的示例第一眼看上去好像它只能跟?Room?一起使用,如果我们不需要Room的话,可能就不想用它了。让我们看一个简单的例子,证明其实并不是这样。

假设我们想写一些测试应用来测试我们的API。我们不想使用任何的数据库或者存储,但是仍然希望高效的做这件事情,不让它一次性加载完所有数据,尽管目的只是测试,那也是相当恐怖的。你第一时间会想到什么?是不是?onScrollListener?之类的技术 , 或者是在??onBindViewHolder?中判断是否应该开始获取数据?总之,你是在思考如何按需获取分页数据。但是你在思考的时候忘记了参考我们的API。好了,让我们看看可以从这个库中得到什么。

我们准备用?Kitsu?API作为例子,任务很简单,就是用列表显示API获取的内容,是的我们的API支持分页。

Screen页面

首先我们需要一个带有列表的Activity:

class?KitsuMainActivity?:?AppCompatActivity()?{
????private?val?viewModel?by?lazy?{?ViewModelProviders.of(this).get(KitsuViewModel::class.java)?}

????override?fun?onCreate(savedInstanceState:?Bundle?)?{
????????super.onCreate(savedInstanceState)
????????setContentView(R.layout.activity_main)
????????initFlysearch(savedInstanceState)
????????searchForResults("Android")
????}

????private?fun?initKitsu()?{
????????val?kitsuAdapter?=?KitsuPagedListAdapter()
????????searchResultsRecyclerView.adapter?=?kitsuAdapter
????????viewModel.allKitsu.observe(this,?Observer(kitsuAdapter::setList))
????}

????private?fun?searchForResults(queryFilter:?String)?{
????????viewModel.setQueryFilter(queryFilter)
????????initKitsu()
????}
}

KitsuMainActivity.kt?hosted with ? by?GitHub

没什么特别之处。只有一个简单的ViewModel

ViewModel

现在让我们看看ViewModel的实现:

class?KitsuViewModel(app:?Application)?:?AndroidViewModel(app)?{
????private?var?allKitsuLiveData:?LiveData<>>??=?null
????val?allKitsu:?LiveData<>>
????????get()?{
????????????if?(null?==?allKitsuLiveData)?{
????????????????allKitsuLiveData?=?KitsuMediaPagedListProvider.allKitsu().create(0,
????????????????????????PagedList.Config.Builder()
????????????????????????????????.setPageSize(PAGED_LIST_PAGE_SIZE)
????????????????????????????????.setInitialLoadSizeHint(PAGED_LIST_PAGE_SIZE)
????????????????????????????????.setEnablePlaceholders(PAGED_LIST_ENABLE_PLACEHOLDERS)
????????????????????????????????.build())!!
????????????}
????????????return?allKitsuLiveData??:?throw?AssertionError("Check?your?threads?...")
????????}

????fun?setQueryFilter(queryFilter:?String)?{
????????KitsuMediaPagedListProvider.setQueryFilter(queryFilter)
????????allKitsuLiveData?=?null?//?invalidate
????}

????companion?object?{
????????private?const?val?PAGED_LIST_PAGE_SIZE?=?20
????????private?const?val?PAGED_LIST_ENABLE_PLACEHOLDERS?=?false
????}
}

KitsuViewModel.kt?hosted with ? by?GitHub

这里要注意的是我们把placeholders禁用了(setEnablePlaceholders(false)),为什么要这样做呢?因为如果你要用placeholders来显示empty view的话,我们必须指定一个明确的item数目,而这里无法知道到底有多少个item。实际上item的个数我们使用的是?(DataSource#COUNT_UNDEFINED) 。

PagedListProvider

让我们来看看?PagedList?的provider:

object?KitsuMediaPagedListProvider?{
????private?val?dataSource?=?object:?KitsuLimitOffsetNetworkDataSource(KitsuRestApi)?{
????????override?fun?convertToItems(items:?KitsuResponse,?size:?Int):?List?{
????????????return?List(size,?{?index?->
????????????????items.data.elementAtOrElse(index,?{?KitsuItem(0,?null,?null)?})
????????????})
????????}
????}

????fun?allKitsu():?LivePagedListProvider?{
????????return?object?:?LivePagedListProvider()?{
????????????override?fun?createDataSource():?KitsuLimitOffsetNetworkDataSource?=?dataSource
????????}
????}

????fun?setQueryFilter(queryFilter:?String)?{
????????dataSource.queryFilter?=?queryFilter
????}
}

rawKitsuMediaPagedListProvider.kt?hosted with ? by?GitHub

这里我们创建了一个自定义的DataSource对象,实现了它的抽象方法convertToItems(items: KitsuResponse, size: Int),该方法用于将从数据源获得的数据转换成List。为了能够改变查询的关键词,我们把它作为一个单独的变量。最后我们使用这个datasource创建?LivePagedListProvider?,稍后我们将使用它来创建LiveData。你可能也注意到了,这里我们传入了 API object ,使用?Retrofit?来获取数据。

Data

数据是什么样的呢?并不神秘,只是从Retrofit调用转换而来的简单的数据:

class?KitsuResponse(
????????val?data:?List)

data?class?KitsuItem(
????????val?id:?Int,
????????val?type:?String?,
????????val?attributes:?KitsuItemAttributes?)

data?class?KitsuItemAttributes(
????????val?synopsis:?String?,
????????val?subtype:?String?,
????????val?titles:?KitsuItemAttributesTitles?,
????????val?posterImage:?KitsuItemAttributesImage?)

data?class?KitsuItemAttributesTitles(
????????val?en_jp:?String?)

data?class?KitsuItemAttributesImage(
????????val?small:?String?)

KitsuData.kt?hosted with ? by?GitHub

Datasource

现在该看看我们自定义的DataSource抽象类长什么样了:

abstract?class?KitsuLimitOffsetNetworkDataSource?protected?constructor(
????????val?dataProvider:?KitsuRestApi)?:?TiledDataSource()?{

????var?queryFilter:?String?=?""

????override?fun?countItems():?Int?=?DataSource.COUNT_UNDEFINED

????protected?abstract?fun?convertToItems(items:?KitsuResponse,?size:?Int):?List

????override?fun?loadRange(startPosition:?Int,?loadCount:?Int):?List??{
????????val?response?=?dataProvider.getKitsu(queryFilter,?startPosition,?loadCount).execute().body()
????????return?convertToItems(response,?response.data.size)
????}
}

KitsuLimitOffsetNetworkDataSource.kt?hosted with ? by?GitHub

abstract?class?KitsuLimitOffsetNetworkDataSource?protected?constructor(
????????val?dataProvider:?KitsuRestApi)?:?TiledDataSource()?{

????var?queryFilter:?String?=?""

????override?fun?countItems():?Int?=?DataSource.COUNT_UNDEFINED

????protected?abstract?fun?convertToItems(items:?KitsuResponse,?size:?Int):?List

????override?fun?loadRange(startPosition:?Int,?loadCount:?Int):?List??{
????????val?response?=?dataProvider.getKitsu(queryFilter,?startPosition,?loadCount).execute().body()
????????return?convertToItems(response,?response.data.size)
????}
}

KitsuLimitOffsetNetworkDataSource.kt?hosted with ? by?GitHub

为了方便起见我们使用?TiledDataSource?,但是这可能不是正确的方式,因为TiledDataSource需要提供明确的item数目。但是因为我们禁用了placeholder,所以它将被转换成ContiguousDataSource,然后一切就很方便了。

因为我们的API是支持分页的,所以你会发现DataSource的?loadRange?方法实现起来太简单了。而且在loadRange方法中做耗时操作也是可以的,因为它是在后台线程被调用的。

API

api调用的相关代码是这样的:

object?KitsuRestApi?{
????private?val?kitsuApi:?KitsuSpecApi

????init?{
????????val?retrofit?=?Retrofit.Builder()
????????????????.baseUrl("https://kitsu.io/api/edge/")
????????????????.addConverterFactory(MoshiConverterFactory.create())
????????????????.build()

????????kitsuApi?=?retrofit.create(KitsuSpecApi::class.java)
????}

????fun?getKitsu(filter:?String,?offset:?Int,?limit:?Int):?Call?{
????????return?kitsuApi.filterKitsu(filter,?limit,?offset)
????}
}

interface?KitsuSpecApi?{
????@GET("anime")
????fun?filterKitsu(
????????????@Query("filter[text]")?filter:?String,
????????????@Query("page[limit]")?limit:?Int,
????????????@Query("page[offset]")?offset:?Int):?Call
}

KitsuAPI.kt?hosted with ? by?GitHub

Adapter & ViewHolder

我觉得到这里就基本完成了。再来看看UI部分的Adapter 和?ViewHolder:

class?KitsuViewHolder(parent?:ViewGroup)?:?RecyclerView.ViewHolder(
????????LayoutInflater.from(parent.context).inflate(R.layout.kitsu_item,?parent,?false))?{

????var?item?:?KitsuItem??=?null

????fun?bindTo(item?:?KitsuItem?)?{
????????this.item?=?item
????????itemView.itemTypeView.text?=?item?.type?.capitalize()
?????????????????:?"Ouhh..."
????????itemView.itemSubtypeView.text?=?item?.attributes?.subtype?.capitalize()
?????????????????:?"Ouhhhhh..."
????????itemView.itemNameView.text?=?item?.attributes?.titles?.en_jp?.capitalize()
?????????????????:?"Ouhhhhhhhh..."
????????itemView.itemSynopsisView.text?=?item?.attributes?.synopsis?.capitalize()
?????????????????:?"Ouhhhhhhhhhhh...\nYou?know?what?\nThe?quick?brown?fox?jumps?over?the?lazy?dog!"
????????val?imageUrl?=?item?.attributes?.posterImage?.small
????????if?(null?!=?imageUrl)?{
????????????itemView.itemCoverView.visibility?=?View.VISIBLE
????????????Glide.with(itemView.context)
????????????????????.load(imageUrl)
????????????????????.apply(RequestOptions().placeholder(R.drawable.empty_placeholder))
????????????????????.transition(DrawableTransitionOptions.withCrossFade())
????????????????????.into(itemView.itemCoverView)
????????}?else?{
????????????Glide.with(itemView.context).clear(itemView.itemCoverView)
????????????itemView.itemCoverView.setImageResource(R.drawable.empty_placeholder)
????????}
????}
}

class?KitsuPagedListAdapter?:?PagedListAdapter(diffCallback)?{
????override?fun?onBindViewHolder(holder:?KitsuViewHolder,?position:?Int)?{
????????holder.bindTo(getItem(position))
????}

????override?fun?onCreateViewHolder(parent:?ViewGroup,?viewType:?Int):?KitsuViewHolder?=?KitsuViewHolder(parent)

????companion?object?{
????????private?val?diffCallback?=?object?:?DiffCallback()?{
????????????override?fun?areItemsTheSame(oldItem:?KitsuItem,?newItem:?KitsuItem):?Boolean?=?oldItem.id?==?newItem.id
????????????override?fun?areContentsTheSame(oldItem:?KitsuItem,?newItem:?KitsuItem):?Boolean?=?oldItem?==?newItem
????????}
????}
}

KitsuUIParts.kt?hosted with ? by?GitHub

这里的ViewHolder没有什么好说的,唯一有点怪异的就是有许多“Oughhh…”。有点意思的是KitsuPagedListAdapter,它继承了?PagedListAdapter?。因为这里所有东西都是基于页面的,这个adapter是一个特殊的类。我们还提供了DiffCallback来对比item,利用?diff 算法?高效的处理列表中发生的变化。

效果

终于完成了,下面是效果:

Untitled.gif

译者注:其实看不出来分页效果对吧,因为它并没有加载等待的提示。

对了,差点忘记提一下?Chris Craik?告诉我们的话:

Paging alpha1 doesn’t drop data?—?wanted to get that in, but wasn’t able to for the first alpha. Will be added in the future, and the in-memory max count will be configurable.

更新:别忘了去收听?Florina Muntenescu?参与的关于?Android Architecture Paging Library?的节目!点击这里

源码地址:https://github.com/brainail/.samples/tree/master/ArchPagingLibraryWithNetwork?

上一篇:【译】使用Kotlin和RxJava测试MVP架构的完整示例 - 第1部分
原文链接: https://android.jlelse.eu/complete-example-of-testing-mvp-architecture-with-kotlin-and-rxjava-part-1-816e22e71ff4 简书译文地址: http://www.jianshu.com/p/6d88998316b1 最近我创建了一个playground项目来了解更多关于Kotlin和RxJava的信
下一篇:从概念设计到安卓实现, 第二部分(译)
自从上一篇文章发布之后已经有一段时日了,虽然期间经历了很多事情,但是最终还是来了,希望你们依旧喜欢! 这是我的“从设计到android”系列的新篇,如果你记得这个系列的 第一部分 ,就应该知道当时我选了一个有趣的概念设计,并尝试在Android 上实现它,