Paging3:从RemoteMediator获取数据后,回收器视图闪烁并且某些项目移动到位

问题描述 投票:0回答:1

我正在构建一个电影应用程序,它使用 Paging3 使用 Remote Mediator 同时从网络和本地数据库进行寻呼。
从 TMDB api 获取数据 并将它们保存到房间数据库中。

但是我在回收器视图中遇到一些闪烁或闪烁
当我向下滚动时,某些项目会改变其位置并上下跳跃。

这是有关该问题的视频:https://youtu.be/TzV9Mf85uzk

仅使用 api 或数据库的分页源即可正常工作。
但是当使用远程中介时,将任何页面数据从 api 插入到数据库后就会发生闪烁。

我不知道是什么原因造成的,希望我能找到解决方案。

这是我的一些代码片段:

远程中介器

class MovieRemoteMediator(
    private val query: String ="",
    private val repository: MovieRepository
) :
    RemoteMediator<Int, Movie>() {

    companion object {
        private const val STARTING_PAGE_INDEX = 1
    }

    private val searchQuery = query.ifEmpty { "DEFAULT_QUERY" }


    override suspend fun initialize(): InitializeAction {
        // Require that remote REFRESH is launched on initial load and succeeds before launching
        // remote PREPEND / APPEND.
        return InitializeAction.LAUNCH_INITIAL_REFRESH
    }


    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, Movie>
    ): MediatorResult {

        val page = when (loadType) {
            LoadType.REFRESH -> STARTING_PAGE_INDEX
            LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
            LoadType.APPEND -> {
                val remoteKey = getRemoteKeyForLastItem(state)
                val nextPage = remoteKey?.nextPage
                    ?: return MediatorResult.Success(endOfPaginationReached = remoteKey != null)
                nextPage
            }
        }
        val response = repository.getMoviesFromApi(page)

        if (response is NetworkResult.Success) {
            val movies = response.data.results ?: emptyList()
            val nextPage: Int? =
                if (response.data.page < response.data.totalPages) response.data.page + 1 else null

            val remoteKeys: List<MovieRemoteKey> = movies.map { movie ->
                MovieRemoteKey(searchQuery, movie.id, nextPage)
            }
            repository.insertAndDeleteMoviesAndRemoteKeysToDB(
                searchQuery,
                movies,
                remoteKeys,
                loadType
            )


            return MediatorResult.Success(
                endOfPaginationReached = nextPage == null
            )
        } else {
            val error = (response as NetworkResult.Error).errorMessage
            return MediatorResult.Error(Exception(error))
        }


    }



    private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Movie>): MovieRemoteKey? {

        return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
            ?.let { movie ->
                repository.getMovieRemoteKey(movie.id.toInt(), searchQuery)
            }

    }


} 

存储库

class MovieRepository @Inject constructor(
    private val apiClient: ApiService,
    private val movieDao: MovieDao,
    private val movieRemoteKeyDao: MovieRemoteKeyDao
) {

    companion object {
        private const val PAGE_SIZE =20
        val config = PagingConfig(pageSize = PAGE_SIZE,
        enablePlaceholders = false)
    }

    @OptIn(ExperimentalPagingApi::class)
    fun getPagingMovies() = Pager(config,
            remoteMediator = MovieRemoteMediator(repository = this)
        ) {
        getPagedMoviesFromDB(SortType.DEFAULT, "")
            }.flow


    suspend fun insertAndDeleteMoviesAndRemoteKeysToDB(
        query: String,
        movies: List<Movie>,
        remoteKeys: List<MovieRemoteKey>,
        loadType: LoadType
    )= withContext(Dispatchers.IO) {
        movieRemoteKeyDao.insertAndDeleteMoviesAndRemoteKeys(query,movies, remoteKeys, loadType)
    }


    suspend fun getMovieRemoteKey(itemId:Int,query:String):MovieRemoteKey? {
        return movieRemoteKeyDao.getRemoteKey(itemId,query)
    }
     

电影道

  fun getSortedMovies(sortType: SortType, searchQuery: String) : Flow<List<Movie>> =
        when(sortType){
            SortType.ASC ->  getSortedMoviesASC(searchQuery)
            SortType.DESC -> getSortedMoviesDESC(searchQuery)
            SortType.DEFAULT -> getMovies()
        }

    fun getPagedMovies(sortType: SortType, searchQuery: String) : PagingSource<Int,Movie> =
        when(sortType){
            SortType.ASC ->  getPagedSortedMoviesASC(searchQuery)
            SortType.DESC -> getPagedSortedMoviesDESC(searchQuery)
            SortType.DEFAULT -> getDefaultPagedMovies(searchQuery.ifEmpty { "DEFAULT_QUERY" })
        }


    @Query("SELECT * FROM movies ORDER BY popularity DESC")
    fun getMovies(): Flow<List<Movie>>

    @Query("SELECT * FROM movies WHERE title LIKE '%' || :search || '%'" +
            " OR originalTitle LIKE :search" +
            " ORDER BY title ASC")
    fun getSortedMoviesASC(search:String): Flow<List<Movie>>

    @Query("SELECT * FROM movies WHERE title LIKE '%' || :search || '%'" +
            " OR originalTitle LIKE :search" +
            " ORDER BY title DESC")
    fun getSortedMoviesDESC(search:String): Flow<List<Movie>>
    
    @Transaction
    @Query("SELECT * FROM movies" +
            " INNER JOIN movie_remote_key_table on movies.id = movie_remote_key_table.movieId" +
            " WHERE searchQuery = :search" +
            " ORDER BY movie_remote_key_table.id")
    fun getDefaultPagedMovies(search:String): PagingSource<Int,Movie>

    @Query("SELECT * FROM movies WHERE title LIKE '%' || :search || '%'" +
            " OR originalTitle LIKE :search" +
            " ORDER BY title ASC")
    fun getPagedSortedMoviesASC(search:String): PagingSource<Int,Movie>

    @Query("SELECT * FROM movies WHERE title LIKE '%' || :search || '%'" +
            " OR originalTitle LIKE :search" +
            " ORDER BY title DESC")
    fun getPagedSortedMoviesDESC(search:String): PagingSource<Int,Movie>



    @Query("SELECT * FROM movies WHERE id = :id")
    fun getMovieById(id: Int): Flow<Movie>


    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertMovie(movie: Movie)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertMovies(movies: List<Movie>)

    @Query("DELETE FROM movies")
    fun deleteAllMovies()

    @Query("DELETE FROM movies WHERE id = :id")
    fun deleteMovieById(id: Int)

}


RemoteKeyDao


@Dao
interface MovieRemoteKeyDao {

    @Query("SELECT * FROM movie_remote_key_table WHERE movieId = :movieId AND searchQuery = :query LIMIT 1")
    suspend fun getRemoteKey(movieId: Int, query: String): MovieRemoteKey?


    @Query("DELETE FROM movie_remote_key_table WHERE searchQuery = :query")
    suspend fun deleteRemoteKeys(query: String)

    @Transaction
    @Query("DELETE FROM movies WHERE id IN ( SELECT movieId FROM movie_remote_key_table WHERE searchQuery = :query)")
    suspend fun deleteMoviesByRemoteKeys(query: String)

    @Transaction
    suspend fun insertAndDeleteMoviesAndRemoteKeys(
        query: String,
        movies: List<Movie>,
        remoteKeys: List<MovieRemoteKey>,
        loadType: LoadType
    ) {

        if (loadType == LoadType.REFRESH) {
            Timber.d("REMOTE SOURCE DELETING:")

            deleteMoviesByRemoteKeys(query)
            deleteRemoteKeys(query)

        }
        Timber.d("REMOTE SOURCE INSERTING ${movies.size} Movies and ${remoteKeys.size} RemoteKeys :")

        insertMovies(movies)
        insertRemoteKey(remoteKeys)


    }


    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertRemoteKey(movieRemoteKey: List<MovieRemoteKey>)


    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertMovies(movies: List<Movie>)


}

电影视图模型


@HiltViewModel
class MoviesViewModel @Inject constructor(
    private val repository: MovieRepository, private val preferenceManger: PreferenceManger
) : ViewModel() {

    private val searchFlow = MutableStateFlow("")
    private val sortFlow = preferenceManger.preferencesFlow

    val movies = repository.getPagingMovies().cachedIn(viewModelScope)
//
//    val movies: StateFlow<Resource<List<Movie>>> = sortFlow.combine(searchFlow) { sort, search ->
//        Pair(sort, search)
//    }    //For having timeouts for search query so not overload the server
//        .debounce(600)
//        .distinctUntilChanged()
//        .flatMapLatest { (sort, search) ->
//            repository.getMovies(sort, search)
//        }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Resource.Loading())
//

    fun setSearchQuery(query: String) {
        searchFlow.value = query
    }

    fun saveSortType(type: SortType) {
        viewModelScope.launch {
            preferenceManger.saveSortType(type)
        }
    }

    private val _currentMovie = MutableLiveData<Movie?>()
    val currentMovie: LiveData<Movie?>
        get() = _currentMovie

    fun setMovie(movie: Movie?) {
        _currentMovie.value = movie
    }

}

电影片段



@AndroidEntryPoint
class MoviesFragment : Fragment(), MovieClickListener {
    private lateinit var moviesBinding: FragmentMoviesBinding
    private lateinit var pagingMovieAdapter: PagingMovieAdapter
    private val viewModel: MoviesViewModel by activityViewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View {
        moviesBinding = FragmentMoviesBinding.inflate(inflater, container, false)
        return moviesBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setupUI()
        getMovies()
    }

    private fun setupUI() {
        pagingMovieAdapter = PagingMovieAdapter(this)
        moviesBinding.moviesRv.layoutManager = GridLayoutManager(context, 3)

        moviesBinding.moviesRv.adapter = pagingMovieAdapter
        moviesBinding.moviesRv.setHasFixedSize(true)

        setHasOptionsMenu(true)
    }


    private fun getMovies() {

//        repeatOnLifeCycle(pagingMovieAdapter.loadStateFlow) { loadStates ->
//            val state = loadStates.refresh
//            moviesBinding.loadingView.isVisible = state is LoadState.Loading
//
//            if (state is LoadState.Error) {
//                val errorMsg = state.error.message
//                Toast.makeText(context, errorMsg, Toast.LENGTH_LONG).show()
//            }
//
//        }


        lifecycleScope.launchWhenCreated{
            viewModel.movies.collectLatest { pagingMovieAdapter.submitData(it) }
        }
      //  repeatOnLifeCycle(viewModel.movies,pagingMovieAdapter::submitData)

//        //scroll to top after updating the adapter
//        repeatOnLifeCycle(pagingMovieAdapter.loadStateFlow
//            .distinctUntilChangedBy { it.refresh }
//            .filter { it.refresh is LoadState.NotLoading }
//        ) {
//            moviesBinding.moviesRv.scrollToPosition(0)
//        }
    }


    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.main, menu)

        val searchView = menu.findItem(R.id.action_search).actionView as SearchView

        searchView.onQueryTextChanged() { query ->
            viewModel.setSearchQuery(query)
        }

        super.onCreateOptionsMenu(menu, inflater)
    }


    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_sort_asc -> {
                viewModel.saveSortType(SortType.ASC)
                true
            }
            R.id.action_sort_desc -> {
                viewModel.saveSortType(SortType.DESC)
                true
            }
            R.id.action_sort_default -> {
                viewModel.saveSortType(SortType.DEFAULT)
                true
            }
            else -> super.onOptionsItemSelected(item)

        }


    }





    override fun onMovieClickListener(movie: Movie?) {
        Toast.makeText(context, movie?.title, Toast.LENGTH_SHORT).show()
        viewModel.setMovie(movie)

        movie?.id?.let {
            val action = MoviesFragmentDirections.actionMoviesFragmentToDetailsFragment2(it)
            findNavController().navigate(action)
        }
    }
}

分页电影适配器


class PagingMovieAdapter(private val movieClickListener: MovieClickListener)
    : PagingDataAdapter<Movie, PagingMovieAdapter.PagingMovieViewHolder>(diffUtil) {

    companion object{
        val diffUtil = 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): PagingMovieViewHolder {
        return PagingMovieViewHolder.from(parent,movieClickListener)
    }

    override fun onBindViewHolder(holder: PagingMovieViewHolder, position: Int) {
        val movie = getItem(position)
        holder.bind(movie)

    }


    class PagingMovieViewHolder(private val movieBinding: ItemMovieBinding,private val movieClickListener: MovieClickListener) :
        RecyclerView.ViewHolder(movieBinding.root) , View.OnClickListener{

        init {
            movieBinding.root.setOnClickListener(this)
        }

        fun bind(movie: Movie?)  {
            movie.let { movieBinding.movie = movie }
        }

        companion object {
            fun from(parent: ViewGroup, movieClickListener: MovieClickListener): PagingMovieViewHolder {
                val inflater = LayoutInflater.from(parent.context)
                val movieBinding = ItemMovieBinding.inflate(inflater, parent, false)
                return PagingMovieViewHolder(movieBinding,movieClickListener)
            }
        }

        override fun onClick(p0: View?) {
            val  movie = movieBinding.movie
            movieClickListener.onMovieClickListener(movie)
        }
    }


}

谢谢。

android kotlin android-paging-3
1个回答
0
投票

对于任何可能面临此问题的人,

闪烁是因为我的

diffUtil
回调在
areContentTheSame
上返回 false,因为我的数据模型类上有一个长数组参数 和 kotlin 数据类 equals 方法根据它们的引用而不是值来比较数组,所以我必须手动重写 equals 方法。

对于在其位置移动的项目,这是因为我禁用了占位符 在分页配置上 这使得分页库在更新数据库后返回错误的偏移量 所以制作

enablePlaceholders = false
解决了这个问题。

来自 api 的数据顺序与来自数据库的数据顺序不同也可能会导致此问题。

谢谢

© www.soinside.com 2019 - 2024. All rights reserved.