前言
之前写过一篇关于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
文中的Pixabay
的key
最好自行申请,不然多人使用过多会有上限
-
创建
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 ) { } }
-
创建
DataSourceFactory
继承
DataSource.Factory
,重写create()
方法class PixabayDataSourceFactory(private val context: Context) : DataSource.Factory
() { override fun create(): DataSource { return PixabayDataSource(context) } }
-
在
ViewModel
中获取class GalleryViewModel(application: Application) : AndroidViewModel(application) { val pagedListPhoto = PixabayDataSourceFactory(application).toLiveData(1) }
-
最后在
Activity/Fragment
中调用即可galleryViewModel.pagedListPhoto.observe(viewLifecycleOwner, Observer { galleryAdapter.submitList(it) })
扩展一:网络状态
-
在
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 ) { } }
-
在
DataSourceFactory
中,我们依然使用LiveData
进行观察private val _pixabayDataSOurce = MutableLiveData
() val pixabayDataSOurce: LiveData = _pixabayDataSOurce override fun create(): DataSource { return PixabayDataSource(context) .also { _pixabayDataSOurce.postValue(it) } }
-
在
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
观察对象
-
在
ViewModel
观察即可galleryViewModel.networkStatus.observe(viewLifecycleOwner, Observer { Log.d("networkStatus", it.toString()) })
运行应用后断开网络连接,往下华东加载图片,查看控制台日志变化
扩展二:重连
当用户因为网络而无法继续加载时,需要提供用户一个重连的机会
-
在
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) } }
-
在
ViewModel
中获取fun retry() { factory.pixabayDataSOurce.value?.retry?.invoke() }
最后在
Activity/Fragment
中调用即可
扩展三:优雅重连
当加载列表时网络断开了,需要在列表底部添加一个重试按钮,更加人性化
-
创建底部布局
gallery_footer.xml
-
修改
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
-
剩下就是加载的问题了,直接看代码
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() } } } } } }
结尾
基本内容就是这些了,这篇文章只是大致描述了过程,如果看不明白的最好还是看视频来的直观点
(好久好久没更新了,太忙了(太懒了))