Android 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<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 算法 高效的处理列表中发生的变化。

效果

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

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

来自:Android Architecture Components