使用 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
    }
}