前面我们使用过Paging,最新版本Paging3和以前对比,有所改动
Paging2->Paging3三个模块改为:
1.DataSource->PagingSource : 数据从该模块中获取,数据可以来源于网络、本地数据库等
2.PagedList->Pager : 负责具体获取数据的逻辑,何时获取、加载下一页、预加载等
3.PagedListAdapter->PagingDataAdapter : RecyclerView的adapter需要继承它,内部做了一系列处理
一、paging3上手
效果:
1.首先配置gradle
使用kapt插件
plugins {
id 'kotlin-kapt'
}
DataBinding支持
defaultConfig {
dataBinding{
enabled = true
}
}
添加依赖
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
//依赖协程核心库 ,提供Android UI调度器
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1"
//依赖当前平台所对应的平台库 (必须)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01'
implementation 'androidx.paging:paging-runtime-ktx:3.0.0-beta03'
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
2.根据服务器返回数据创建实体类
服务器数据:
{
"has_more":true,
"subjects":[
{
"id":35076714,
"title":"扎克·施奈德版正义联盟",
"cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2634360594.webp",
"rate":"8.9"
},
{
"id":26935283,
"title":"侍神令",
"cover":"https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2629260713.webp",
"rate":"5.8"
},
{
"id":35145068,
"title":"双层肉排",
"cover":"https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2633977758.webp",
"rate":"6.7"
},
{
"id":33433405,
"title":"大地",
"cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2628845704.webp",
"rate":"6.6"
},
{
"id":35167535,
"title":"租来的朋友",
"cover":"https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2616903233.webp",
"rate":"6.1"
}
]
}
package com.aruba.paging3application.entity
/**
* Created by aruba on 2021/9/22.
*/
data class Movie(
val id: Int,
val title: String,
val cover: String,
val rate: String
)
package com.aruba.paging3application.entity
import com.google.gson.annotations.SerializedName
/**
* Created by aruba on 2021/9/22.
*/
data class Movies(
@SerializedName("subjects")
val movies: List<Movie>,
@SerializedName("has_more")
val hasMore: Boolean
)
3.定义网络相关
Api:
package com.aruba.flowapplyapplication.net
import com.aruba.paging3application.entity.Movies
import retrofit2.http.GET
import retrofit2.http.Query
/**
* Created by aruba on 2021/9/21.
*/
interface Api {
@GET("pkds.do")
suspend fun getMovies(
@Query("page") page: Int,
@Query("pagesize") pagesize: Int
): Movies
}
retrofit工具类:
package com.aruba.flowapplyapplication.net
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
/**
* Created by aruba on 2021/9/21.
*/
object RetrofitClient {
private val instance: Retrofit by lazy {
Retrofit.Builder()
.baseUrl("http://192.168.17.114:8080/pagingserver_war/")
.client(OkHttpClient.Builder().build())
.addConverterFactory(GsonConverterFactory.create())
.build()
}
fun <T> getApi(clazz: Class<T>): T {
return instance.create(clazz) as T
}
}
4.定义RecyclerView的Adapter相关
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="10dip">
<ImageView
android:id="@+id/imageView"
android:layout_width="100dip"
android:layout_height="100dip"
app:image="@{movie.cover}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.432"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.054"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/textViewTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{movie.title}"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.255"
tools:text="泰坦尼克号" />
<TextView
android:id="@+id/textViewRate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@{movie.rate}"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textViewTitle"
tools:text="评分:8.9分" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.4" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>
<data>
<variable
name="movie"
type="com.aruba.paging3application.entity.Movie" />
</data>
</layout>
BingingAdapter,自定义标签image:
package com.aruba.paging3application.bindingAdapter
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import com.aruba.paging3application.R
import com.squareup.picasso.Picasso
/**
* Created by aruba on 2021/9/22.
*/
@BindingAdapter("image")
fun setImage(imageView: ImageView, imageUrl: String) {
Picasso.get().load(imageUrl).placeholder(R.drawable.ic_launcher_foreground)
.error(R.drawable.ic_launcher_foreground)
.into(imageView);
}
Adapter,继承于PagingDataAdapter,和paging2一样需要DiffUtil.ItemCallback:
package com.aruba.paging3application.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.DifferCallback
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import com.aruba.paging3application.databinding.ItemBinding
import com.aruba.paging3application.entity.Movie
/**
* Created by aruba on 2021/9/22.
*/
class MoviePagingAdapter : PagingDataAdapter<Movie, BindingViewHolder>(
object : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem == newItem
}
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder {
val binding = ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingViewHolder(binding)
}
override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
(holder.binding as ItemBinding).movie = getItem(position)
}
}
5.定义PagingSource
继承PagingSource,实现load函数,返回值为LoadResult,可以使用LoadResult.Page实例化,入参为继承时定义的第二个泛型,和上一页和下一页的两个Key,Key对应的第一个泛型
package com.aruba.paging3application.paging
import android.util.Log
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aruba.flowapplyapplication.net.Api
import com.aruba.flowapplyapplication.net.RetrofitClient
import com.aruba.paging3application.entity.Movie
import com.aruba.paging3application.entity.Movies
/**
* Pagind获取数据层
* Created by aruba on 2021/9/22.
*/
class MoviePagingSource : PagingSource<Int, Movie>() {
companion object {
const val pageSize = 10
}
//该办法只在初始加载成功且加载页面的列表不为空的情况下被调用。
override fun getRefreshKey(state: PagingState<Int, Movie>): Int? {
return null
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> {
val currentPage = params.key ?: 1
val movies = RetrofitClient.getApi(Api::class.java).getMovies(currentPage, pageSize)
//上一页的key
val prevKey: Int? = (currentPage - 1).run {
if (this == 0) {//说明当前是第一页数据
null
} else {
this
}
}
//下一页的key
val nextKey: Int? = when {
params.loadSize > pageSize -> {//第一次加载的会多一些
// public val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER,
// 默认PagingConfig为pager分配初始获取数据的大小为pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER
// 所以Pager配置时,如果initialLoadSize不指定,那么第一次加载数据并不是我们定义的pageSize
val count = params.loadSize / pageSize
count + 1
}
movies.hasMore -> {
currentPage + 1
}
else -> {
null
}
}
return LoadResult.Page(
data = movies.movies,
prevKey = prevKey,
nextKey = nextKey
)
}
}
6.定义Pager
Pager可以返回一个Flow,使用一个ViewModel获取Pager的Flow,下流就可以收集了
package com.aruba.paging3application.viewmodel
import androidx.lifecycle.ViewModel
import androidx.paging.Pager
import androidx.paging.PagingConfig
import com.aruba.paging3application.paging.MoviePagingSource
/**
* Created by aruba on 2021/9/22.
*/
class MovieViewModel : ViewModel() {
//绑定page的flow,返回是一个flow对象
fun bindPage() = Pager(config = PagingConfig(
initialLoadSize = MoviePagingSource.pageSize,//初次加载的数据量大小
pageSize = MoviePagingSource.pageSize,
enablePlaceholders = false
),
pagingSourceFactory = {
MoviePagingSource()
}
).flow
}
7.Activity中实例化ViewModel、配置RecyclerView等
package com.aruba.paging3application
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.aruba.paging3application.adapter.MoviePagingAdapter
import com.aruba.paging3application.databinding.ActivityMainBinding
import com.aruba.paging3application.viewmodel.MovieViewModel
import kotlinx.coroutines.flow.collect
class MainActivity : AppCompatActivity() {
private val movieViewModel by viewModels<MovieViewModel>()
private val binding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
val adapter = MoviePagingAdapter()
binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(this)
lifecycleScope.launchWhenCreated {
movieViewModel.bindPage().collect {
adapter.submitData(it)
}
}
}
}
二、加载更多
效果:
LoadStateAdapter
PagingDataAdapter支持设置一个LoadStateAdapter,来显示加载更多
定义Adapter继承于LoadStateAdapter:
package com.aruba.paging3application.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter
import com.aruba.paging3application.databinding.MovieLoadmoreBinding
/**
* Created by aruba on 2021/9/22.
*/
class LoadMoreAdapter : LoadStateAdapter<BindingViewHolder>() {
override fun onBindViewHolder(holder: BindingViewHolder, loadState: LoadState) {
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): BindingViewHolder {
val binding = MovieLoadmoreBinding.inflate(
LayoutInflater.from(parent.context),
parent, false
)
return BindingViewHolder(binding)
}
}
布局为:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:padding="10dp">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="20dp"
android:layout_height="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tv_loading" />
<TextView
android:id="@+id/tv_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="正在加载数据... ..."
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
PagingDataAdapter设置下就可以了
val adapter = MoviePagingAdapter()
binding.recyclerView.adapter = adapter.withLoadStateFooter(LoadMoreAdapter())
三、下拉刷新
效果:
在布局中为RecyclerView套一层SwipeRefreshLayout后,在Activity中设置刷新监听
binding.apply {
recyclerView.adapter = adapter.withLoadStateFooter(LoadMoreAdapter())
recyclerView.layoutManager = LinearLayoutManager(this@MainActivity)
//开始刷新
refreshLayout.setOnRefreshListener {
adapter.refresh()
}
}
对Adapter的状态进行监听,来更新SwipeRefreshLayout的刷新状态
//监听adapter状态
adapter.loadStateFlow.collect {
//根据刷新状态来通知swiprefreshLayout是否刷新完毕
binding.refreshLayout.isRefreshing = it.refresh is LoadState.Loading
}
完整代码:
package com.aruba.paging3application
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.recyclerview.widget.LinearLayoutManager
import com.aruba.paging3application.adapter.LoadMoreAdapter
import com.aruba.paging3application.adapter.MoviePagingAdapter
import com.aruba.paging3application.databinding.ActivityMainBinding
import com.aruba.paging3application.viewmodel.MovieViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private val movieViewModel by viewModels<MovieViewModel>()
private val binding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
val adapter = MoviePagingAdapter()
binding.apply {
recyclerView.adapter = adapter.withLoadStateFooter(LoadMoreAdapter())
recyclerView.layoutManager = LinearLayoutManager(this@MainActivity)
//开始刷新
refreshLayout.setOnRefreshListener {
adapter.refresh()
}
}
lifecycleScope.launchWhenCreated {
launch {
movieViewModel.bindPage().collect {
adapter.submitData(it)
}
}
launch {
//监听adapter状态
adapter.loadStateFlow.collect {
//根据刷新状态来通知swiprefreshLayout是否刷新完毕
binding.refreshLayout.isRefreshing = it.refresh is LoadState.Loading
}
}
}
}
}
四、瞬态数据缓存
目前我们写的代码是不带瞬态数据缓存的:
想要实现瞬态数据缓存,则要将Flow变为ViewModel的属性,并为它设置作用域
package com.aruba.paging3application.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import com.aruba.paging3application.paging.MoviePagingSource
/**
* Created by aruba on 2021/9/22.
*/
class MovieViewModel : ViewModel() {
private val pagerFlow by lazy {
Pager(config = PagingConfig(
initialLoadSize = MoviePagingSource.pageSize * 2,//初次加载的数据量大小
pageSize = MoviePagingSource.pageSize,
enablePlaceholders = false
),
pagingSourceFactory = {
MoviePagingSource()
}
).flow.cachedIn(viewModelScope)
}
//绑定page的flow,返回是一个flow对象
fun bindPage() = pagerFlow
}
效果:
网友评论