Paging3: представление Recycler мигает, и некоторые элементы перемещаются на свои места после получения данных от RemoteAndroid

Форум для тех, кто программирует под Android
Ответить
Anonymous
 Paging3: представление Recycler мигает, и некоторые элементы перемещаются на свои места после получения данных от Remote

Сообщение Anonymous »

Я создаю приложение для фильмов, которое использует Paging3 для одновременной загрузки страниц из сети и из локальной базы данных с помощью Remote Mediator.

который получает данные из API TMDB
и сохраняет их в базе данных комнаты.

Но у меня наблюдается мигание или мерцание в представлении переработчика

когда я прокручиваю вниз, некоторые элементы меняют свое положение и подпрыгивают вверх и вниз.

вот видео проблемы:

Работа с источником подкачки только для API или базы данных работает нормально.

но при использовании удаленного посредника мигание происходит после вставки любых данных страницы из API в базу данных.

Я не знаю, что является причиной этого, надеюсь, я смогу найти решение.

Вот некоторые из моих фрагментов кода:

RemoteMediator

Код: Выделить всё

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

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
): 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 = 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): 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,
remoteKeys: List,
loadType: LoadType
)= withContext(Dispatchers.IO) {
movieRemoteKeyDao.insertAndDeleteMoviesAndRemoteKeys(query,movies, remoteKeys, loadType)
}

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

MovieDao

Код: Выделить всё

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

fun getPagedMovies(sortType: SortType, searchQuery: String) : PagingSource =
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

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

@Query("SELECT * FROM movies WHERE title LIKE '%' || :search || '%'" +
" OR originalTitle LIKE :search" +
" ORDER BY title DESC")
fun getSortedMoviesDESC(search:String): Flow

@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

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

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

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

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

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

@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,
remoteKeys: List,
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)

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

}

MoviesViewModel

Код: Выделить всё

@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  = 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()
val currentMovie: LiveData
get() = _currentMovie

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

}
MovieFragment

Код: Выделить всё


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

Адаптер PagingMovie

Код: Выделить всё

class PagingMovieAdapter(private val movieClickListener: MovieClickListener)
: PagingDataAdapter(diffUtil) {

companion object{
val diffUtil = object : DiffUtil.ItemCallback() {
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)
}
}

}
Спасибо.

Подробнее здесь: https://stackoverflow.com/questions/728 ... -getting-d
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Android»