代码讲解
Flow在很多地方都与Paging3结合使用,而且Paging3与Paging2也有很大的不同。所以这里讲解下。主要有以下内容
Paging3的结构组成
Flow与Paging3
下拉刷新
上拉加载更多与离奇bug的解决
上游数据缓存
数据从PagingSource来,Pager里设置PageConfig,加载完后会得到Flow,最后交给PagingDataAdapter更新UI
我们看下接口返回的数据
根据接口返回的数据我们设计两个实体类。
package com.dongnaoedu.jetpackpaging.model
import android.icu.text.CaseMap
data class Movie(val id: Int, val title: String, val rate: String, val cover: String)
package com.dongnaoedu.jetpackpaging.model
import com.google.gson.annotations.SerializedName
/**
* 服务器响应的结果,是一个Json数据
*/
data class Movies(
@SerializedName("subjects")
val movieList: List<Movie>,
@SerializedName("has_more")
val hasMore: Boolean//是否还有下一页
)
我们在看下服务器要求的字段
根据服务器要求的我们设计我们访问网络的接口
package com.dongnaoedu.jetpackpaging.net
import com.dongnaoedu.jetpackpaging.model.Movies
import retrofit2.http.GET
import retrofit2.http.Query
interface MovieApi {
//返回是个Movies对象,Movies对象里面有集合
@GET("pkds.do")
suspend fun getMovies(
@Query("page") page: Int,
@Query("pagesize") pageSize: Int
): Movies
}
代码结构
先看下目录结构
Activity代码
package com.dongnaoedu.jetpackpaging.activity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import com.dongnaoedu.jetpackpaging.adapter.MovieAdapter
import com.dongnaoedu.jetpackpaging.adapter.MovieLoadMoreAdapter
import com.dongnaoedu.jetpackpaging.databinding.ActivityMainBinding
import com.dongnaoedu.jetpackpaging.viewmodel.MovieViewModel
import kotlinx.coroutines.flow.collectLatest
class MainActivity : AppCompatActivity() {
private val movieViewModel by viewModels<MovieViewModel>()
private val mBinding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mBinding.root)
val movieAdapter = MovieAdapter(this)
mBinding.apply {
//withLoadStateFooter加载更多脚布局,需要一个适配器做参数
//context要这么写this@MainActivity,单单写个this指的是mBinding对象
recyclerView.adapter = movieAdapter.withLoadStateFooter(MovieLoadMoreAdapter(this@MainActivity))
swipeRefreshLayout.setOnRefreshListener {
//刷新就重新获取数据
movieAdapter.refresh()
}
}
lifecycleScope.launchWhenCreated {
//因为不断的刷新,有可能有多个值,我们这里只取最新的数据
movieViewModel.loadMovie().collectLatest {
movieAdapter.submitData(it)
}
}
lifecycleScope.launchWhenCreated {
//监听刷新。数据拿到了就停止刷新。loadStateFlow可以获取状态
movieAdapter.loadStateFlow.collectLatest { state ->
//判断state.refreh是否是LoadState.Loading
mBinding.swipeRefreshLayout.isRefreshing = state.refresh is LoadState.Loading
}
}
}
}
Adapter代码之BindingViewHolder
package com.dongnaoedu.jetpackpaging.adapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
class BindingViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root)
Adapter之ImageViewBindingAdapter
package com.dongnaoedu.jetpackpaging.adapter
import android.graphics.Color
import android.text.TextUtils
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import com.dongnaoedu.jetpackpaging.R
import com.squareup.picasso.Picasso
/**
* 自定义图片加载
*/
class ImageViewBindingAdapter {
//必须是静态方法才能完成绑定,java调用Kotlin静态方法
//需要两步,第一步使用companion object,第二步使用@JvmStatic注解
companion object {
@JvmStatic
@BindingAdapter("image")
fun setImage(imageView: ImageView, url: String) {
if (!TextUtils.isEmpty(url)) {
Picasso.get().load(url).placeholder(R.drawable.ic_launcher_background)
.into(imageView)
} else {
imageView.setBackgroundColor(Color.GRAY)
}
}
}
}
Adapter之MovieAdapter
package com.dongnaoedu.jetpackpaging.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import com.dongnaoedu.jetpackpaging.databinding.MovieItemBinding
import com.dongnaoedu.jetpackpaging.model.Movie
//继承的是PagingDataAdapter,自定义一个BindingViewHolder
//PagingDataAdapter的构造方法有三个参数,mainDispatcher ,workerDispatcher和DiffUtil.ItemCallback
//前两个参数(调度器)是默认实现的,第三个参数需要我们自己实现,我们这里使用对象表达式
class MovieAdapter(private val context: Context) :
PagingDataAdapter<Movie, BindingViewHolder>(object : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem.id == newItem.id//ID相同我们就认为是同一个Item
}
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
//Movie是的data数据类,会重写比较内容的方法。比较的是Movie的属性值是否都相等
// kotlin === 引用 , == 内容
// Java == 引用, equals 内容
return oldItem == newItem
}
}) {
override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
val movie = getItem(position)
movie?.let {
val binding = holder.binding as MovieItemBinding
binding.movie = it
binding.networkImage = it.cover
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder {
val binding = MovieItemBinding.inflate(LayoutInflater.from(context), parent, false)
return BindingViewHolder(binding)
}
}
Adapter之MovieLoadMoreAdapter
package com.dongnaoedu.jetpackpaging.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter
import com.dongnaoedu.jetpackpaging.databinding.MovieLoadmoreBinding
/**
*
* @author ningchuanqi
* @version V1.0
*/
class MovieLoadMoreAdapter(private val context: Context) : LoadStateAdapter<BindingViewHolder>() {
override fun onBindViewHolder(holder: BindingViewHolder, loadState: LoadState) {
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): BindingViewHolder {
val binding = MovieLoadmoreBinding.inflate(LayoutInflater.from(context), parent, false)
return BindingViewHolder(binding)
}
}
model之Movie代码
package com.dongnaoedu.jetpackpaging.model
import android.icu.text.CaseMap
/**
*
*/
data class Movie(val id: Int, val title: String, val rate: String, val cover: String)
model代码之Movies
package com.dongnaoedu.jetpackpaging.model
import com.google.gson.annotations.SerializedName
/**
* 服务器响应的结果,是一个Json数据
*/
data class Movies(
@SerializedName("subjects")
val movieList: List<Movie>,
@SerializedName("has_more")
val hasMore: Boolean//是否还有下一页
)
net代码之MovieApi
package com.dongnaoedu.jetpackpaging.net
import com.dongnaoedu.jetpackpaging.model.Movies
import retrofit2.http.GET
import retrofit2.http.Query
interface MovieApi {
//返回是个Movies对象,Movies对象里面有集合
@GET("pkds.do")
suspend fun getMovies(
@Query("page") page: Int,
@Query("pagesize") pageSize: Int
): Movies
}
net代码之RetrofitClient
package com.dongnaoedu.jetpackpaging.net
import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
/**
*
* @author ningchuanqi
* @version V1.0
*/
object RetrofitClient {
private val instance: Retrofit by lazy {
val interceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger {
//Log.d("ning", it)
})
interceptor.level = HttpLoggingInterceptor.Level.BODY
Retrofit.Builder()
.client(OkHttpClient.Builder().addInterceptor(interceptor).build())
.baseUrl("http://192.168.1.4:8080/pagingserver_war/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
fun <T> createApi(clazz: Class<T>): T {
return instance.create(clazz) as T
}
}
paging代码之MoviePagingSource
package com.dongnaoedu.jetpackpaging.paging
import android.util.Log
import androidx.paging.PagingSource
import com.dongnaoedu.jetpackpaging.model.Movie
import com.dongnaoedu.jetpackpaging.net.MovieApi
import com.dongnaoedu.jetpackpaging.net.RetrofitClient
import kotlinx.coroutines.delay
/**
*
* @author ningchuanqi
* @version V1.0
*/
class MoviePagingSource : PagingSource<Int, Movie>() {
//load就是要请求我的服务器,返回的是LoadResult<Int, Movie>对象
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> {
delay(2000)//如果看不到加载更多的脚布局,可以增加这句代码
//如果params.key为空,就是第一次加载,我们让currentPage等于1
val currentPage = params.key ?: 1
//一页有多少数据
val pageSize = params.loadSize
//在这里请求服务器,传入当前的page,和pageSize。
val movies = RetrofitClient.createApi(MovieApi::class.java).getMovies(currentPage, pageSize)
Log.d("ning", "currentPage:$currentPage,pageSize:$pageSize")
var prevKey: Int? = null
var nextKey: Int? = null
val realPageSize = 8//每页的数据
val initialLoadSize = 16
if (currentPage == 1) {
prevKey = null
//nextKey为一次性加载的页数+1。比如我们一次性加载两页,那么下一页就是2+1
nextKey = initialLoadSize / realPageSize + 1
} else {
prevKey = currentPage - 1
//如果有下一页就+1,否则为null。
nextKey = if (movies.hasMore) currentPage + 1 else null
}
Log.d("ning", "prevKey:$prevKey,nextKey:$nextKey")
return try {
LoadResult.Page(
data = movies.movieList,
prevKey = prevKey,
nextKey = nextKey
)
} catch (e: Exception) {
e.printStackTrace()
return LoadResult.Error(e)
}
}
}
viewmodel代码之MovieViewModel
package com.dongnaoedu.jetpackpaging.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.dongnaoedu.jetpackpaging.model.Movie
import com.dongnaoedu.jetpackpaging.paging.MoviePagingSource
import kotlinx.coroutines.flow.Flow
class MovieViewModel : ViewModel() {
//注意,viewModel的临时数据要放在属性上保存。如果不放到属性上保存
//每次切换横竖屏都会重新加载。所以这里一定要这么写(A和B都不能忘记)。
private val movies by lazy {//A处声明一个成员变量
// Pager有一个flow对象
Pager(
config = PagingConfig(
pageSize = 8,
//距离最后一个Item多远时候加载数据,默认为PageSize,一定要大于一。太小的话会有Bug.给LoadMore预留足够的位置
prefetchDistance = 8,
initialLoadSize = 16//默认是3 X pagesize
),
pagingSourceFactory = {MoviePagingSource()}
//流的数据要缓存必须使用cachedIn
//流会一直是活跃的只要我们给定的作用域是活跃的,activity没有挂掉,viewmodeScope就会一直在
//The flow is kept active as long as the given [scope] is active
).flow.cachedIn(viewModelScope)//B处要用cachedIn缓存
}
//在ViewModel中请求,返回类型是Flow<PagingData<Movie>>
fun loadMovie() : Flow<PagingData<Movie>> = movies
}
布局代码之activity_main
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
布局代码之movie_item
<?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">
<data>
<variable
name="networkImage"
type="String" />
<variable
name="movie"
type="com.dongnaoedu.jetpackpaging.model.Movie" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="10dip">
<ImageView
app:image="@{networkImage}"
android:id="@+id/imageView"
android:layout_width="100dip"
android:layout_height="100dip"
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: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="泰坦尼克号"
android:text="@{movie.title}"/>
<TextView
android:id="@+id/textViewRate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textViewTitle"
tools:text="评分:8.9分"
android:text="@{movie.rate}"/>
<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>
</layout>
布局代码之movie_loadmore
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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>