Android Paging Library按页获取网络数据实例
新的 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<PagedList<KitsuItem>>? = null
val allKitsu: LiveData<PagedList<KitsuItem>>
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<KitsuItem>(KitsuRestApi) {
override fun convertToItems(items: KitsuResponse, size: Int): List<KitsuItem> {
return List(size, { index ->
items.data.elementAtOrElse(index, { KitsuItem(0, null, null) })
})
}
}
fun allKitsu(): LivePagedListProvider<Int, KitsuItem> {
return object : LivePagedListProvider<Int, KitsuItem>() {
override fun createDataSource(): KitsuLimitOffsetNetworkDataSource<KitsuItem> = 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<KitsuItem>)
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<T> protected constructor(
val dataProvider: KitsuRestApi) : TiledDataSource<T>() {
var queryFilter: String = ""
override fun countItems(): Int = DataSource.COUNT_UNDEFINED
protected abstract fun convertToItems(items: KitsuResponse, size: Int): List<T>
override fun loadRange(startPosition: Int, loadCount: Int): List<T>? {
val response = dataProvider.getKitsu(queryFilter, startPosition, loadCount).execute().body()
return convertToItems(response, response.data.size)
}
}
KitsuLimitOffsetNetworkDataSource.kt hosted with ❤ by GitHub
abstract class KitsuLimitOffsetNetworkDataSource<T> protected constructor(
val dataProvider: KitsuRestApi) : TiledDataSource<T>() {
var queryFilter: String = ""
override fun countItems(): Int = DataSource.COUNT_UNDEFINED
protected abstract fun convertToItems(items: KitsuResponse, size: Int): List<T>
override fun loadRange(startPosition: Int, loadCount: Int): List<T>? {
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<KitsuResponse> {
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<KitsuResponse>
}
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<KitsuItem, KitsuViewHolder>(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<KitsuItem>() {
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 算法 高效的处理列表中发生的变化。
效果
终于完成了,下面是效果:
译者注:其实看不出来分页效果对吧,因为它并没有加载等待的提示。
对了,差点忘记提一下 Chris Craik 告诉我们的话:
更新:别忘了去收听 Florina Muntenescu 参与的关于 Android Architecture Paging Library 的节目!点击这里。
源码地址:https://github.com/brainail/.samples/tree/master/ArchPagingLibraryWithNetwork