Paging库的使用

前言

之前写过一篇关于Paging的文章,结合Room数据库使用的

Paging

这篇内容是关于请求网络数据的。Pging是一个分页加载库,其中组重要的部分就是PagedList。通过DataSource 获取的数据传递给PagedList对象;PagedList再通过PagedListAdapter显示到RecyclerView

其中DataSource分为三种:

  • PageKeyedDataSource:一般用于加载带有上下页的数据
  • ItemKeyedDataSource:一般用于上下文关联的数据,即通过上一个来获取下一个
  • PositionalDataSource:用于请求指定位置的一组数据

更详细的可以看官网介绍:custom-data-source

这里我在放下UP主的视频嘿嘿,大家可以直接去看

第47集 利用Paging实现加载更多(1)

第48集 利用Paging实现加载更多(2)

第49集 利用Paging实现加载更多(3)

正文

对了还有几点补充下:

本文内容都是基于UP主上一期视频的项目,有需要可以点此下载

该项目使用的是PageKeyedDataSource

网络请求库使用的是Volley

文中的Pixabaykey最好自行申请,不然多人使用过多会有上限

  1. 创建DataSource

    继承PageKeyedDataSource,Key为Int,Value为请求的数据对象

    重写三个方法,分别是:初始化、加载上一页、加载下一页

    class PixabayDataSource(private val context: Context) : PageKeyedDataSource() {
    
        private val keyWords = arrayOf("cat", "dog", "car", "beauty", "phone", "computer", "flower", "animal")
    
        override fun loadInitial(
            params: LoadInitialParams,
            callback: LoadInitialCallback
        ) {
            val url =
                "https://pixabay.com/api/?key=17534539-6000088804093f98505e5666b&q=${keyWords.random()}&per_page=20&page=1"
            StringRequest(
                Request.Method.GET,
                url,
                Response.Listener {
                    val dataList = Gson().fromJson(it, Pixabay::class.java).hits.toList()
                    // onResult(List data, Key previousPageKey, Key nextPageKey)
                    callback.onResult(dataList, null, 2)
                },
                Response.ErrorListener {
                    Log.d("hello",it.toString())
                }
            ).also {
                VolleySingleton.getInstance(context).requestQueue.add(it)
            }
        }
    
        override fun loadAfter(params: LoadParams, callback: LoadCallback) {
            val url =
                "https://pixabay.com/api/?key=17534539-6000088804093f98505e5666b&q=${keyWords.random()}&per_page=20&page=${params.key}"
            StringRequest(
                Request.Method.GET,
                url,
                Response.Listener {
                    val dataList = Gson().fromJson(it, Pixabay::class.java).hits.toList()
                    // onResult(List data, Key adjacentPageKey)
                    callback.onResult(dataList, params.key + 1)
                },
                Response.ErrorListener {
                    Log.d("hello",it.toString())
                }
            ).also {
                VolleySingleton.getInstance(context).requestQueue.add(it)
            }
        }
    
        override fun loadBefore(params: LoadParams, callback: LoadCallback) {
        }
    }
    
  1. 创建DataSourceFactory

    继承DataSource.Factory,重写create()方法

    class PixabayDataSourceFactory(private val context: Context) : DataSource.Factory() {
        override fun create(): DataSource {
            return PixabayDataSource(context)
        }
    }
    
  1. ViewModel中获取

    class GalleryViewModel(application: Application) : AndroidViewModel(application) {
        val pagedListPhoto = PixabayDataSourceFactory(application).toLiveData(1)
    }
    
  1. 最后在Activity/Fragment中调用即可

    galleryViewModel.pagedListPhoto.observe(viewLifecycleOwner, Observer {
     galleryAdapter.submitList(it)
    })
    

扩展一:网络状态

  1. DataSource中定义网络枚举类,同时设置一个MutableLiveData_networkStatus,供外部进行观察

    enum class NetworkStatus {
        LOADING,
        FAILED,
        COMPLETED
    }
    
    class PixabayDataSource(private val context: Context) : PageKeyedDataSource() {
    
        private val keyWords = arrayOf("cat", "dog", "car", "beauty", "phone", "computer", "flower", "animal")
        private val _networkStatus = MutableLiveData()
        val networkStatus: LiveData = _networkStatus
    
        override fun loadInitial(
            params: LoadInitialParams,
            callback: LoadInitialCallback
        ) {
            _networkStatus.postValue(NetworkStatus.LOADING)
            val url =
                "https://pixabay.com/api/?key=17534539-6000088804093f98505e5666b&q=${keyWords.random()}&per_page=20&page=1"
            StringRequest(
                Request.Method.GET,
                url,
                Response.Listener {
                    val dataList = Gson().fromJson(it, Pixabay::class.java).hits.toList()
                    callback.onResult(dataList, null, 2)
                },
                Response.ErrorListener {
                    _networkStatus.postValue(NetworkStatus.FAILED)
                    Log.d("hello",it.toString())
                }
            ).also {
                VolleySingleton.getInstance(context).requestQueue.add(it)
            }
        }
    
        override fun loadAfter(params: LoadParams, callback: LoadCallback) {
            _networkStatus.postValue(NetworkStatus.LOADING)
            val url =
                "https://pixabay.com/api/?key=17534539-6000088804093f98505e5666b&q=${keyWords.random()}&per_page=20&page=${params.key}"
            StringRequest(
                Request.Method.GET,
                url,
                Response.Listener {
                    val dataList = Gson().fromJson(it, Pixabay::class.java).hits.toList()
                    callback.onResult(dataList, params.key + 1)
                },
                Response.ErrorListener {
                    _networkStatus.postValue(NetworkStatus.FAILED)
                    Log.d("hello",it.toString())
                }
            ).also {
                VolleySingleton.getInstance(context).requestQueue.add(it)
            }
        }
    
        override fun loadBefore(params: LoadParams, callback: LoadCallback) {
        }
    }
    
  1. DataSourceFactory中,我们依然使用LiveData进行观察

    private val _pixabayDataSOurce = MutableLiveData()
    val pixabayDataSOurce: LiveData = _pixabayDataSOurce
    override fun create(): DataSource {
        return PixabayDataSource(context)
        .also { _pixabayDataSOurce.postValue(it) }
    }
    
  1. ViewModel中进行传递

    class GalleryViewModel(application: Application) : AndroidViewModel(application) {
    
        private val factory = PixabayDataSourceFactory(application)
        val pagedListPhoto = factory.toLiveData(1)
        val networkStatus = Transformations.switchMap(factory.pixabayDataSOurce, {it.networkStatus})
        fun resetQuery() {
            pagedListPhoto.value?.dataSource?.invalidate()
        }
    }
    

    这里说一下这个Transformations.switchMap()。它内部通过MediatorLiveData实现

    MediatorLiveData继承自MutableLiveData,作用就是观察其他LiveData的变化

    switchMap可以理解成:A派B去监听C,你只需要告知我C的信息,所以文中可以获得DataSOurce中的

networkStatus观察对象

  1. ViewModel观察即可

    galleryViewModel.networkStatus.observe(viewLifecycleOwner, Observer {
        Log.d("networkStatus", it.toString())
    })
    

    运行应用后断开网络连接,往下华东加载图片,查看控制台日志变化

扩展二:重连

当用户因为网络而无法继续加载时,需要提供用户一个重连的机会

  1. DataSource中保存初始化函数,

    var retry: (() -> Any)? = null // 类型为函数
    
    ... ...
    
    override fun loadInitial(
        params: LoadInitialParams,
        callback: LoadInitialCallback
    ) {
        retry = null // 发起请求时清空retry
        _networkStatus.postValue(NetworkStatus.LOADING)
        val url =
        "https://pixabay.com/api/?key=17534539-6000088804093f98505e5666b&q=${keyWords.random()}&per_page=20&page=1"
        StringRequest(
            Request.Method.GET,
            url,
            Response.Listener {
                val dataList = Gson().fromJson(it, Pixabay::class.java).hits.toList()
                callback.onResult(dataList, null, 2)
            },
            Response.ErrorListener {
                retry = {loadInitial(params, callback)} // 请求失败保存
                _networkStatus.postValue(NetworkStatus.FAILED)
                Log.d("hello",it.toString())
            }
        ).also {
            VolleySingleton.getInstance(context).requestQueue.add(it)
        }
    }
    
    
  1. ViewModel中获取

    fun retry() {
        factory.pixabayDataSOurce.value?.retry?.invoke()
    }
    

    最后在Activity/Fragment中调用即可

扩展三:优雅重连

当加载列表时网络断开了,需要在列表底部添加一个重试按钮,更加人性化

  1. 创建底部布局gallery_footer.xml

    
    
    
        
    
        
    
    
  1. 修改GalleryAdapter

    class GalleryAdapter(private val galleryViewModel: GalleryViewModel) :
        PagedListAdapter(DIFFCALLBACK) {
    
        private var hasFooter = false 
        private var networkStatus: NetworkStatus? = null
    
        // 供外部调用,更新网络状态
        fun updateNetworkStatus(networkStatus: NetworkStatus) {
            this.networkStatus = networkStatus
            // 第一次加载的时候不需要显示
            if (networkStatus == NetworkStatus.INITIAL_LOADING) {
                hideFooter()
            } else {
                showFooter()
            }
        }
    
        private fun hideFooter() {
            // r如果当前已存在,则进行移除
            if (hasFooter) {
                notifyItemRemoved(itemCount - 1)
            }
            hasFooter = false
        }
    
        private fun showFooter() {
            // 如果已存在,则改变状态
            if (hasFooter) {
                notifyItemChanged(itemCount - 1)
            } else {
                hasFooter = true
                notifyItemInserted(itemCount - 1)
            }
        }
    
        override fun getItemCount(): Int {
            return super.getItemCount() + if (hasFooter) 1 else 0
        }
    
        override fun getItemViewType(position: Int): Int {
            return if (hasFooter && position == itemCount - 1) R.layout.gallery_footer else R.layout.gallery_cell
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    
            return when (viewType) {
                R.layout.gallery_cell -> PhotoViewHolder.getInstance(parent).also { holder ->
                    holder.itemView.setOnClickListener {
                        Bundle().apply {
                            putInt("PHOTO_POSITION", holder.adapterPosition)
                            holder.itemView.findNavController()
                            .navigate(R.id.action_galleryFragment_to_pagerPhotoFragment, this)
                        }
                    }
                }
                else -> FooterViewHolder.getInstance(parent).also {
                    it.itemView.setOnClickListener {
                        galleryViewModel.retry()
                    }
                }
            }
        }
    
        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            when (holder.itemViewType) {
                R.layout.gallery_footer -> (holder as FooterViewHolder)
                .bindWithNetworkStatus(networkStatus)
                else -> {
                    val photoItem = getItem(position) ?: return
                    (holder as PhotoViewHolder).bindWithPhotoItem(photoItem)
                }
            }
        }
    
        object DIFFCALLBACK : DiffUtil.ItemCallback() {
            override fun areItemsTheSame(oldItem: PhotoItem, newItem: PhotoItem): Boolean {
                return oldItem === newItem
            }
    
            override fun areContentsTheSame(oldItem: PhotoItem, newItem: PhotoItem): Boolean {
                return oldItem.photoId == newItem.photoId
            }
        }
    
    }
    
    // 将原先onCreateViewHolder以及onBindViewHolder中的部分抽离出来
    class PhotoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    
        companion object {
    
            fun getInstance(parent: ViewGroup): PhotoViewHolder {
                val view = LayoutInflater.from(parent.context)
                    .inflate(R.layout.gallery_cell, parent, false)
                return PhotoViewHolder(view)
            }
        }
    
        fun bindWithPhotoItem(photoItem: PhotoItem) {
            with(itemView) {
                shimmerLayoutCell.apply {
                    setShimmerColor(0x55FFFFFF)
                    setShimmerAngle(0)
                    startShimmerAnimation()
                }
                textViewUser.text = photoItem.photoUser
                textViewLikes.text = photoItem.photoLikes.toString()
                textViewFavorites.text = photoItem.photoFavorites.toString()
                imageView.layoutParams.height = photoItem.photoHeight
            }
            itemView.shimmerLayoutCell.apply {
                setShimmerColor(0x55FFFFFF)
                setShimmerAngle(0)
                startShimmerAnimation()
            }
            Glide.with(itemView)
                .load(photoItem.previewUrl)
                .placeholder(R.drawable.photo_placeholder)
                .listener(object : RequestListener {
                    override fun onLoadFailed(
                        e: GlideException?,
                        model: Any?,
                        target: Target?,
                        isFirstResource: Boolean
                    ): Boolean {
                        return false
                    }
    
                    override fun onResourceReady(
                        resource: Drawable?,
                        model: Any?,
                        target: Target?,
                        dataSource: DataSource?,
                        isFirstResource: Boolean
                    ): Boolean {
                        return false.also { 
                            itemView.shimmerLayoutCell?.stopShimmerAnimation()
                        }
                    }
    
                })
                .into(itemView.imageView)
        }
    
    }
    
    class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    
        companion object {
    
            fun getInstance(parent: ViewGroup): FooterViewHolder {
                val view = LayoutInflater.from(parent.context)
                    .inflate(R.layout.gallery_footer, parent, false)
                // 
                (view.layoutParams as StaggeredGridLayoutManager.LayoutParams).isFullSpan = true
                return FooterViewHolder(view)
            }
        }
    
        // 根据网络加载不同状态
        fun bindWithNetworkStatus(networkStatus: NetworkStatus?) {
            with(itemView) {
                when (networkStatus) {
                    NetworkStatus.FAILED -> {
                        textView.text = "点击重试"
                        progressBar.visibility = View.GONE
                        isClickable = true
                    }
                    NetworkStatus.COMPLETED -> {
                        textView.text = "加载完毕"
                        progressBar.visibility = View.GONE
                        isClickable = false
                    }
                    else -> {
                        textView.text = "正在加载"
                        progressBar.visibility = View.VISIBLE
                        isClickable = false
                    }
                }
            }
        }
    }
    

    上面还需要对DataSource进行修改,添加一种网络:INITIAL_LOADING,代表初次加载,同时loadInitial中的网络状态改为INITIAL_LOADING

  1. 剩下就是加载的问题了,直接看代码

    class GalleryFragment : Fragment() {
        // 此处的viewmodel作用域提升到Activity
        private val galleryViewModel by activityViewModels()
    
        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            // Inflate the layout for this fragment
            return inflater.inflate(R.layout.fragment_gallery, container, false)
        }
    
        override fun onOptionsItemSelected(item: MenuItem): Boolean {
            when (item.itemId) {
                R.id.swipeIndicator -> {
                    swipeLayoutGallery.isRefreshing = true
                    galleryViewModel.resetQuery()
                }
                R.id.retry -> {
                    galleryViewModel.retry()
                }
            }
    
            return super.onOptionsItemSelected(item)
        }
    
        override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
            super.onCreateOptionsMenu(menu, inflater)
            inflater.inflate(R.menu.menu, menu)
        }
    
        override fun onActivityCreated(savedInstanceState: Bundle?) {
            super.onActivityCreated(savedInstanceState)
            setHasOptionsMenu(true)
            val galleryAdapter = GalleryAdapter(galleryViewModel)
            recyclerView.apply {
                adapter = galleryAdapter
                layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
            }
    
            galleryViewModel.pagedListPhoto.observe(viewLifecycleOwner, Observer {
                galleryAdapter.submitList(it)
                swipeLayoutGallery.isRefreshing = false
            })
    
            swipeLayoutGallery.setOnRefreshListener {
                galleryViewModel.resetQuery()
            }
    
            // 这里监听网络状态,并更新adapter
            galleryViewModel.networkStatus.observe(viewLifecycleOwner, Observer {
                Log.d("networkStatus", it.toString())
                galleryAdapter.updateNetworkStatus(it)
            })
    
        }
    }
    

    PagerPhotoFragment

    const val REQUEST_WRITE_EXTERNAL_STORAGE = 1
    
    class PagerPhotoFragment : Fragment() {
    
        // 同样提升权限
        val galleryViewModel by activityViewModels()
    
        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            // Inflate the layout for this fragment
            return inflater.inflate(R.layout.fragment_pager_photo, container, false)
        }
    
        override fun onActivityCreated(savedInstanceState: Bundle?) {
            super.onActivityCreated(savedInstanceState)
            val adapter = PagerPhotoListAdapter()
            viewPager2.adapter = adapter
            //数据从原来通过bundle传递改为viewmodel进行观察
            galleryViewModel.pagedListPhoto.observe(viewLifecycleOwner, Observer {
                adapter.submitList(it)
                viewPager2.setCurrentItem(arguments?.getInt("PHOTO_POSITION") ?: 0, false)
            })
    
            viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
                override fun onPageSelected(position: Int) {
                    super.onPageSelected(position)
                    photoTag.text = getString(R.string.photo_tag, position + 1, galleryViewModel.pagedListPhoto.value?.size)
                }
            })
    
    
            saveButton.setOnClickListener {
                if (Build.VERSION.SDK_INT < 29 && ContextCompat.checkSelfPermission(
                        requireContext(),
                        Manifest.permission.WRITE_EXTERNAL_STORAGE
                    ) != PackageManager.PERMISSION_GRANTED
                ) {
                    requestPermissions(
                        arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                        REQUEST_WRITE_EXTERNAL_STORAGE
                    )
                } else {
                    viewLifecycleOwner.lifecycleScope.launch {
                        savePhoto()
                    }
                }
            }
        }
    
        override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array,
            grantResults: IntArray
        ) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
            when (requestCode) {
                REQUEST_WRITE_EXTERNAL_STORAGE -> {
                    if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        viewLifecycleOwner.lifecycleScope.launch {
                            savePhoto()
                        }
                    } else {
                        Toast.makeText(requireContext(), "存储失败", Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }
    
        private suspend fun savePhoto() {
            withContext(Dispatchers.IO) {
                val holder =
                    (viewPager2[0] as RecyclerView).findViewHolderForAdapterPosition(viewPager2.currentItem)
                            as PagerPhotoViewHolder
                val bitmap = holder.itemView.pagerPhoto.drawable.toBitmap()
    
                val saveUri = requireContext().contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    ContentValues()
                )?: kotlin.run {
                    Toast.makeText(requireContext(), "存储失败", Toast.LENGTH_SHORT).show()
                    return@withContext
                }
                requireContext().contentResolver.openOutputStream(saveUri).use {
                    if (bitmap.compress(Bitmap.CompressFormat.JPEG,90,it)) {
                        MainScope().launch {Toast.makeText(requireContext(), "存储成功", Toast.LENGTH_SHORT).show() }
                    } else {
                        MainScope().launch { Toast.makeText(requireContext(), "存储失败", Toast.LENGTH_SHORT).show() }
                    }
                }
            }
        }
    }
    

结尾

基本内容就是这些了,这篇文章只是大致描述了过程,如果看不明白的最好还是看视频来的直观点

(好久好久没更新了,太忙了(太懒了))

你可能感兴趣的:(Paging库的使用)