使用 Paging v3 时,多次在回收站视图中填充列表项
Multiple times list item populated in recycler-view, when using Paging v3
我在 https://proandroiddev.com/paging-3-easier-way-to-pagination-part-1-584cad1f4f61 & https://proandroiddev.com/how-to-use-the-paging-3-library-in-android-part-2-e2011070a37d 之后实现了 android-paging v3。
但是我看到数据被多次填充,即使本地数据库中只有 3 条记录。
Notifications list screen
谁能指出我做错了什么?提前致谢。
我的代码如下:
NotificationsFragment
class NotificationsFragment : Fragment() {
private lateinit var binding: FragmentNotificationsBinding
private val alertViewModel: NotificationsViewModel by viewModel()
private val pagingAdapter by lazy { AlertsPagingAdapter() }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentNotificationsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onResume() {
super.onResume()
(activity as MainActivity).setUpCustomToolbar(
getString(R.string.alerts),
""
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
initRecyclerView()
}
private fun initRecyclerView() {
binding.rvAlerts.apply {
adapter = pagingAdapter.withLoadStateFooter(AlertLoadStateAdapter {})
layoutManager = LinearLayoutManager(requireContext())
}
lifecycleScope.launch {
alertViewModel.alertListFlow.collectLatest { pagingData ->
pagingAdapter.submitData(
pagingData
)
}
}
}
}
NotificationsViewModel
class NotificationsViewModel(private val useCase: NotificationsUseCase) : BaseViewModel() {
val alertListFlow = Pager(PagingConfig(1)) { NotificationsPagingSource(useCase) }
.flow
.cachedIn(viewModelScope)
}
NotificationsPagingSource
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.example.demo.model.entity.Notifications
import com.example.demo.NotificationsUseCase
class NotificationsPagingSource(private val useCase: NotificationsUseCase) : PagingSource<Int, Notifications>() {
private companion object {
const val INITIAL_PAGE_INDEX = 0
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Notifications> {
val position = params.key ?: INITIAL_PAGE_INDEX
val randomNotifications : List<Notifications> = useCase.fetchNotifications(params.loadSize)
return LoadResult.Page(
data = randomNotifications ,
prevKey = if (position == INITIAL_PAGE_INDEX) null else position - 1,
nextKey = if (randomAlerts.isNullOrEmpty()) null else position + 1
)
}
override fun getRefreshKey(state: PagingState<Int, Notifications>): Int? {
// We need to get the previous key (or next key if previous is null) of the page
// that was closest to the most recently accessed index.
// Anchor position is the most recently accessed index
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
分页适配器
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
class NotificationsPagingAdapter :
PagingDataAdapter<Notifications, NotificationsPagingAdapter.ItemNotificationsViewHolder>(NotificationsEntityDiff()) {
override fun onBindViewHolder(holder: ItemNotificationsViewHolder, position: Int) {
getItem(position)?.let { userPostEntity -> holder.bind(userPostEntity) }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemNotificationsViewHolder {
return ItemNotificationsViewHolder(
ItemLayoutNotificationsBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
/**
* Viewholder for each Notifications layout item
*/
inner class ItemNotificationsViewHolder(private val binding: ItemLayoutNotificationsBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(alert: Notifications) {
binding.tvMessage.text = alert.title
}
}
class NotificationsEntityDiff : DiffUtil.ItemCallback<Notifications>() {
override fun areItemsTheSame(oldItem: Notifications, newItem: Notifications): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Alert, newItem: Notifications): Boolean =
oldItem == newItem
}
}
通知加载状态适配器
class NotificationsLoadStateAdapter(
private val retry: () -> Unit
) : LoadStateAdapter<NotificationsLoadStateAdapter.LoadStateViewHolder>() {
override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {
val progress = holder.itemView.load_state_progress
val btnRetry = holder.itemView.load_state_retry
val txtErrorMessage = holder.itemView.load_state_errorMessage
btnRetry.isVisible = loadState !is LoadState.Loading
// txtErrorMessage.isVisible = loadState !is LoadState.Loading
progress.isVisible = loadState is LoadState.Loading
if (loadState is LoadState.Error) {
// txtErrorMessage.text = loadState.error.localizedMessage
}
btnRetry.setOnClickListener {
retry.invoke()
}
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder {
return LoadStateViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.layout_load_state_view, parent, false)
)
}
class LoadStateViewHolder(private val view: View) : RecyclerView.ViewHolder(view)
}
道
@Query("SELECT * FROM notifications ORDER BY createdAt DESC LIMIT :size")
fun fetchNotifications(size: Int): List<Notifications>
问题已解决。实际上,Dao查询错误,Paging属性错误。
道
@Query("SELECT * FROM notifications ORDER By createdAt DESC LIMIT :size OFFSET (:page * :size)")
fun fetchAlerts(page: Int, size: Int): List<Notifications>
NotificationsViewModel
class NotificationsViewModel(private val useCase: NotificationUseCase) : BaseViewModel() {
// If you want to load at a time one page keep pageSize 1
val alertListFlow =
Pager(PagingConfig(pageSize = 1)) { createPagingSource() }
.flow
.cachedIn(viewModelScope)
/**
* Set up the paging source and the initial page should be 1
*/
private fun createPagingSource(): BasePagingSource<Notifications> {
return BasePagingSource() { page, pageSize, _ ->
useCase.fetchNotifications(page, pageSize)
}
}
}
并使用 BasePagingSource 而不是 NotificationsPagingSource
/**
* This is the base class for a custom [PagingSource]
*
* @param T the data expected to process
* @property pageSize
* @property initialPage
* @property fetchDataCallback where the concrete API call is executed to fetch the data
*
*/
class BasePagingSource<T : Any>(
private val pageSize: Int = 10,
private val initialPage: Int = 0,
private val fetchDataCallback: suspend (Int, Int, Boolean) -> List<T>
) : PagingSource<Int, T>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
val page = params.key ?: initialPage
val data = fetchDataCallback.invoke(page, pageSize, page == initialPage)
// https://android-developers.googleblog.com/2020/07/getting-on-same-page-with-paging-3.html
val prevKey = null
val nextKey = if (data.isNullOrEmpty() || data.size < pageSize) {
null
} else {
page + 1
}
MyLogger.d("page=$page, params.key=${params.key}, pageSize=$pageSize, prevKey=$prevKey, nextKey=$nextKey, resultSize=${data.size}")
return LoadResult.Page(data, prevKey, nextKey)
}
override fun getRefreshKey(state: PagingState<Int, T>): Int? {
// just return null and eventually it will use the passed initialPage
return null
}
}
我在 https://proandroiddev.com/paging-3-easier-way-to-pagination-part-1-584cad1f4f61 & https://proandroiddev.com/how-to-use-the-paging-3-library-in-android-part-2-e2011070a37d 之后实现了 android-paging v3。 但是我看到数据被多次填充,即使本地数据库中只有 3 条记录。
Notifications list screen
谁能指出我做错了什么?提前致谢。
我的代码如下:
NotificationsFragment
class NotificationsFragment : Fragment() {
private lateinit var binding: FragmentNotificationsBinding
private val alertViewModel: NotificationsViewModel by viewModel()
private val pagingAdapter by lazy { AlertsPagingAdapter() }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentNotificationsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onResume() {
super.onResume()
(activity as MainActivity).setUpCustomToolbar(
getString(R.string.alerts),
""
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
initRecyclerView()
}
private fun initRecyclerView() {
binding.rvAlerts.apply {
adapter = pagingAdapter.withLoadStateFooter(AlertLoadStateAdapter {})
layoutManager = LinearLayoutManager(requireContext())
}
lifecycleScope.launch {
alertViewModel.alertListFlow.collectLatest { pagingData ->
pagingAdapter.submitData(
pagingData
)
}
}
}
}
NotificationsViewModel
class NotificationsViewModel(private val useCase: NotificationsUseCase) : BaseViewModel() {
val alertListFlow = Pager(PagingConfig(1)) { NotificationsPagingSource(useCase) }
.flow
.cachedIn(viewModelScope)
}
NotificationsPagingSource
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.example.demo.model.entity.Notifications
import com.example.demo.NotificationsUseCase
class NotificationsPagingSource(private val useCase: NotificationsUseCase) : PagingSource<Int, Notifications>() {
private companion object {
const val INITIAL_PAGE_INDEX = 0
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Notifications> {
val position = params.key ?: INITIAL_PAGE_INDEX
val randomNotifications : List<Notifications> = useCase.fetchNotifications(params.loadSize)
return LoadResult.Page(
data = randomNotifications ,
prevKey = if (position == INITIAL_PAGE_INDEX) null else position - 1,
nextKey = if (randomAlerts.isNullOrEmpty()) null else position + 1
)
}
override fun getRefreshKey(state: PagingState<Int, Notifications>): Int? {
// We need to get the previous key (or next key if previous is null) of the page
// that was closest to the most recently accessed index.
// Anchor position is the most recently accessed index
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
分页适配器
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
class NotificationsPagingAdapter :
PagingDataAdapter<Notifications, NotificationsPagingAdapter.ItemNotificationsViewHolder>(NotificationsEntityDiff()) {
override fun onBindViewHolder(holder: ItemNotificationsViewHolder, position: Int) {
getItem(position)?.let { userPostEntity -> holder.bind(userPostEntity) }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemNotificationsViewHolder {
return ItemNotificationsViewHolder(
ItemLayoutNotificationsBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
/**
* Viewholder for each Notifications layout item
*/
inner class ItemNotificationsViewHolder(private val binding: ItemLayoutNotificationsBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(alert: Notifications) {
binding.tvMessage.text = alert.title
}
}
class NotificationsEntityDiff : DiffUtil.ItemCallback<Notifications>() {
override fun areItemsTheSame(oldItem: Notifications, newItem: Notifications): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Alert, newItem: Notifications): Boolean =
oldItem == newItem
}
}
通知加载状态适配器
class NotificationsLoadStateAdapter(
private val retry: () -> Unit
) : LoadStateAdapter<NotificationsLoadStateAdapter.LoadStateViewHolder>() {
override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {
val progress = holder.itemView.load_state_progress
val btnRetry = holder.itemView.load_state_retry
val txtErrorMessage = holder.itemView.load_state_errorMessage
btnRetry.isVisible = loadState !is LoadState.Loading
// txtErrorMessage.isVisible = loadState !is LoadState.Loading
progress.isVisible = loadState is LoadState.Loading
if (loadState is LoadState.Error) {
// txtErrorMessage.text = loadState.error.localizedMessage
}
btnRetry.setOnClickListener {
retry.invoke()
}
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder {
return LoadStateViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.layout_load_state_view, parent, false)
)
}
class LoadStateViewHolder(private val view: View) : RecyclerView.ViewHolder(view)
}
道
@Query("SELECT * FROM notifications ORDER BY createdAt DESC LIMIT :size")
fun fetchNotifications(size: Int): List<Notifications>
问题已解决。实际上,Dao查询错误,Paging属性错误。
道
@Query("SELECT * FROM notifications ORDER By createdAt DESC LIMIT :size OFFSET (:page * :size)")
fun fetchAlerts(page: Int, size: Int): List<Notifications>
NotificationsViewModel
class NotificationsViewModel(private val useCase: NotificationUseCase) : BaseViewModel() {
// If you want to load at a time one page keep pageSize 1
val alertListFlow =
Pager(PagingConfig(pageSize = 1)) { createPagingSource() }
.flow
.cachedIn(viewModelScope)
/**
* Set up the paging source and the initial page should be 1
*/
private fun createPagingSource(): BasePagingSource<Notifications> {
return BasePagingSource() { page, pageSize, _ ->
useCase.fetchNotifications(page, pageSize)
}
}
}
并使用 BasePagingSource 而不是 NotificationsPagingSource
/**
* This is the base class for a custom [PagingSource]
*
* @param T the data expected to process
* @property pageSize
* @property initialPage
* @property fetchDataCallback where the concrete API call is executed to fetch the data
*
*/
class BasePagingSource<T : Any>(
private val pageSize: Int = 10,
private val initialPage: Int = 0,
private val fetchDataCallback: suspend (Int, Int, Boolean) -> List<T>
) : PagingSource<Int, T>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
val page = params.key ?: initialPage
val data = fetchDataCallback.invoke(page, pageSize, page == initialPage)
// https://android-developers.googleblog.com/2020/07/getting-on-same-page-with-paging-3.html
val prevKey = null
val nextKey = if (data.isNullOrEmpty() || data.size < pageSize) {
null
} else {
page + 1
}
MyLogger.d("page=$page, params.key=${params.key}, pageSize=$pageSize, prevKey=$prevKey, nextKey=$nextKey, resultSize=${data.size}")
return LoadResult.Page(data, prevKey, nextKey)
}
override fun getRefreshKey(state: PagingState<Int, T>): Int? {
// just return null and eventually it will use the passed initialPage
return null
}
}