簡體   English   中英

Paging3 : Recycler View 閃爍,一些項目在從 RemoteMediator 獲取數據后移動到位

[英]Paging3 : Recycler View blinking and some items move in position after getting data from RemoteMediator

我正在構建一個電影應用程序,它使用 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)

}


遙控鑰匙道


@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>)


}

電影ViewModel


@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)
        }
    }
}

PagingMovieAdapter


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)
        }
    }


}

謝謝。

對於任何可能面臨這個問題的人,

閃爍是因為我的diffUtil回調在areContentTheSame上返回 false,因為我的數據模型類和 kotlin 數據類 equals 方法有一個長數組參數,比較數組基於它們的引用而不是值,所以我不得不手動覆蓋 equals 方法。

對於移動到其位置的項目,這是因為我在分頁配置上禁用了占位符,這使得分頁庫在更新數據庫后返回錯誤的偏移量,因此使enablePlaceholders = false解決了這個問題。

來自 api 的數據的順序也與來自數據庫的數據不同可能導致此問題。

謝謝

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM