Recycler View in Fragment shows no data although PagingSource loggs correct data - android

I am trying to display RESTful Api in recycler view in fragment. When app runs it shows no error but loads nothing in recycler view. I logged response in PagingSource file and it shows correct data but still nothing is displayed in recycler view.
This is my Api interface:
interface Api {
companion object {
const val BASE_URL = "http://be7c232bf30e.ngrok.io"
}
#GET("/posts")
suspend fun searchPosts(
#Query("_page") page: Int,
#Query("_limit") perPage: Int
): List<SocialNetworkPost>
}
This is my PagingSource file:
private const val STARTING_PAGE_INDEX = 1
class PagingSource(private val api: Api) : PagingSource<Int, SocialNetworkPost>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, SocialNetworkPost> {
val position = params.key ?: STARTING_PAGE_INDEX
return try {
val response = api.searchPosts(position, params.loadSize)
LoadResult.Page(
data = response,
prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1,
nextKey = if (response.isEmpty()) null else position + 1
)
} catch (exception: IOException) {
LoadResult.Error(exception)
} catch (exception: HttpException) {
LoadResult.Error(exception)
}
}
}
This is my repository:
#Singleton
class Repository #Inject constructor(private val api: Api) {
fun getPostsResults() = Pager(
config = PagingConfig(
pageSize = 20,
maxSize = 100,
enablePlaceholders = false
),
pagingSourceFactory = { PagingSource(api) }
).liveData
}
This is Fragment in which recyclerView is:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val postAdapter = PostsAdapter(this)
binding.apply {
recyclerView.apply {
setHasFixedSize(true)
itemAnimator = null
adapter = postAdapter.withLoadStateHeaderAndFooter(
header = PostsLoadStateAdapter { postAdapter.retry() },
footer = PostsLoadStateAdapter { postAdapter.retry() }
)
buttonRetry.setOnClickListener {
postAdapter.retry()
}
}
}
viewModel.posts.observe(viewLifecycleOwner) {
postAdapter.submitData(viewLifecycleOwner.lifecycle, it)
}
postAdapter.addLoadStateListener { loadState ->
binding.apply {
progressBar.isVisible = loadState.source.refresh is LoadState.Loading
recyclerView.isVisible = loadState.source.refresh is LoadState.NotLoading
buttonRetry.isVisible = loadState.source.refresh is LoadState.Error
textViewError.isVisible = loadState.source.refresh is LoadState.Error
// empty view
if (loadState.source.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached && postAdapter.itemCount < 1) {
recyclerView.isVisible = false
textViewEmpty.isVisible = true
} else {
textViewEmpty.isVisible = false
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onItemClick(post: SocialNetworkPost) {
val action = HomeFragmentDirections.actionHomeFragmentToDetailsFragment(post)
findNavController().navigate(action)
}
}
This is my repository:
class HomeViewModel #ViewModelInject constructor(private val repository: Repository) : ViewModel() {
val posts = repository.getPostsResults().cachedIn(viewModelScope)
}
This is my adapter:
class PostsAdapter(private val listener: OnItemClickListener) : PagingDataAdapter<SocialNetworkPost, PostsAdapter.PostViewHolder>(POSTS_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
val binding = ItemPostBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PostViewHolder(binding)
}
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
val currentItem = getItem(position)
if (currentItem != null) {
Log.d("PostAdapter", "onBindViewHolder: if")
holder.bind(currentItem)
} else {
Log.d("PostAdapter", "onBindViewHolder: else")
}
}
inner class PostViewHolder(private val binding: ItemPostBinding) : RecyclerView.ViewHolder(binding.root) {
init {
binding.root.setOnClickListener {
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
val item = getItem(position)
if (item != null) listener.onItemClick(item)
}
}
}
fun bind(post: SocialNetworkPost) {
Log.d("PostAdapter", "bind: $post")
binding.apply {
Glide.with(itemView)
.load(post.accountIcon)
.centerCrop()
.transition(DrawableTransitionOptions.withCrossFade())
.error(R.drawable.ic_account)
.into(imageViewProfilePicture)
textViewName.text = post.accountName
textViewDescription.text = post.description
Glide.with(itemView)
.load(post.descriptionImage)
.centerCrop()
.transition(DrawableTransitionOptions.withCrossFade())
.error(R.drawable.empty)
.into(imageViewDescription)
textViewLikesAmount.text = "Likes: ${post.likesAmount}"
textViewCommentsAmount.text = "Comments: ${post.commentsAmount}"
}
}
}
interface OnItemClickListener {
fun onItemClick(post: SocialNetworkPost)
}
companion object {
private val POSTS_COMPARATOR = object : DiffUtil.ItemCallback<SocialNetworkPost>() {
override fun areItemsTheSame(oldItem: SocialNetworkPost, newItem: SocialNetworkPost) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: SocialNetworkPost, newItem: SocialNetworkPost) = oldItem == newItem
}
}
}

RecyclerView needs a layoutManager, you can set it in the xml:
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
or programatically:
recyclerView.layoutManager = LinearLayoutManager(requireContext())

Related

Data From API Has Not Appeared Using Paging 3

I'm learning paging 3, but the data from the API doesn't appear. My code is like below:
interface PokeAPI {
#GET("pokemon")
fun getPokemonList() : Call<PokemonList>
#GET("pokemon")
fun getAllPokemon(
#Query("limit") limit: Int,
#Query("offset") offset: Int) : PokemonList
#GET("pokemon/{name}")
fun getPokemonInfo(
#Path("name") name: String
) : Call<Pokemon>
}
class PokePagingSource(private val apiService: PokeAPI): PagingSource<Int, Result>() {
private companion object {
const val INITIAL_PAGE_INDEX = 1
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Result> {
return try {
val position = params.key ?: INITIAL_PAGE_INDEX
val responseData = apiService.getAllPokemon(position, params.loadSize)
if (responseData.results.isEmpty()) {
Log.e("Response Succeed!", responseData.results.toString())
} else {
Log.e("Response Failed!", responseData.results.toString())
}
LoadResult.Page(
data = responseData.results,
prevKey = if (position == INITIAL_PAGE_INDEX) null else position - 1,
nextKey = if (responseData.results.isNullOrEmpty()) null else position + 1
)
} catch (exception: Exception) {
return LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, Result>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
class PokemonRepository(private val apiService: PokeAPI) {
fun getAllPokemon(): LiveData<PagingData<Result>>{
return Pager(
config = PagingConfig(
pageSize = 10
),
pagingSourceFactory = {
PokePagingSource(apiService)
}
).liveData
}
}
object Injection {
private val api by lazy { RetrofitClient().endpoint }
fun provideRepository(): PokemonRepository {
return PokemonRepository(api)
}
}
class PokemonViewModel(pokemonRepository: PokemonRepository) : ViewModel() {
val allPokemonList: LiveData<PagingData<Result>> =
pokemonRepository.getAllPokemon().cachedIn(viewModelScope)
}
class ViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(PokemonViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return PokemonViewModel(Injection.provideRepository()) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
`class PokemonPagingAdapter(private val context: Context) :
PagingDataAdapter<Result, PokemonPagingAdapter.ViewHolder>(DIFF_CALLBACK) {
private var onItemClick: OnAdapterListener? = null
fun setOnItemClick(onItemClick: OnAdapterListener) {
this.onItemClick = onItemClick
}
class ViewHolder(val binding: AdapterPokemonBinding) : RecyclerView.ViewHolder(binding.root) {
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
AdapterPokemonBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val pokemonData = getItem(position)
if (pokemonData != null) {
holder.binding.apply {
val number = if (pokemonData.url.endsWith("/")) {
pokemonData.url.dropLast(1).takeLastWhile { it.isDigit() }
} else {
pokemonData.url.takeLastWhile { it.isDigit() }
}
val url = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${number}.png"
Glide.with(context)
.load(url)
.transition(DrawableTransitionOptions.withCrossFade())
.centerCrop()
.circleCrop()
.into(ivPokemon)
tvNamePokemon.text = pokemonData.name
btnDetail.setOnClickListener {
onItemClick?.onClick(pokemonData, pokemonData.name, url)
}
}
}
}
companion object {
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Result>() {
override fun areItemsTheSame(
oldItem: Result,
newItem: Result
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: Result,
newItem: Result
): Boolean {
return oldItem.name == newItem.name
}
}
}
interface OnAdapterListener {
fun onClick(data: Result, name: String, url: String)
}
}`
class FragmentPokemon: Fragment(R.layout.fragment_pokemon) {
private var _binding : FragmentPokemonBinding? = null
private val binding get() = _binding!!
private lateinit var dataPagingAdapter: PokemonPagingAdapter
private val viewModel: PokemonViewModel by viewModels {
ViewModelFactory()
}
private lateinit var comm: Communicator
override fun onStart() {
super.onStart()
getData()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentPokemonBinding.bind(view)
val toolBar = requireActivity().findViewById<View>(R.id.tool_bar)
toolBar.visibility = View.VISIBLE
val navBar = requireActivity().findViewById<BottomNavigationView>(R.id.bottom_navigation)
navBar.visibility = View.VISIBLE
comm = requireActivity() as Communicator
setupListPokemon()
}
private fun setupListPokemon(){
dataPagingAdapter = PokemonPagingAdapter(requireContext())
dataPagingAdapter.setOnItemClick(object: PokemonPagingAdapter.OnAdapterListener{
override fun onClick(data: Result, name: String, url: String) {
comm.passDataCom(name, url)
}
})
binding.apply {
rvPokemon.layoutManager = LinearLayoutManager(context)
rvPokemon.setHasFixedSize(true)
rvPokemon.adapter = dataPagingAdapter
}
}
private fun getData(){
viewModel.allPokemonList.observe(viewLifecycleOwner){
dataPagingAdapter.submitData(lifecycle, it)
binding.btnCoba.setOnClickListener { btn ->
if (it == null){
Log.e("ResponseFailed", it.toString())
} else Log.e("ResponseSucceed", it.toString())
}
}
}
}
What's the reason? I have followed the step by step implementation of paging 3 but the data still doesn't appear either.
I don't know the API you are using, but it seems that you are using it incorrectly. The getAllPokemon method has limit and offset parameters and you are calling it like apiService.getAllPokemon(position, params.loadSize), so you are using position as a limit and params.loadSize as an offset.
You should pass params.loadSize as a limit, rename INITIAL_PAGE_INDEX to INITIAL_OFFSET and set it to 0, since your API uses offsets instead of pages (at least it seems so from what you provided). The load function should then look something like this:
// get current offset
val offset = params.key ?: INITIAL_OFFSET
val responseData = apiService.getAllPokemon(limit = params.loadSize, offset = offset)
val prevKey = offset - params.loadSize
val nextKey = offset + params.loadSize

Search Query in android studio(kotlin)

I'm currently following a news app tutorial and I have a problem. When I type in a keyword in the edit text widget, articles related to that keyword shows up in the recycler view but when I erase that keyword to type in another keyword, the articles (in the recycler view) from the previous search query doesn't update and even when I exit the search fragment and open it again,The recycler view remains stagnant instead of disappearing. Can anyone please take a look at my code and let me know what I've done wrong. Thanks in advance.
Here is my code:
Search Fragment
`class SearchNewsFragment : Fragment(R.layout.fragment_search_news) {
lateinit var viewModel: NewsViewModel
lateinit var newsAdapter: NewsAdapter
val TAG = "SearchNewsFragment"
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = (activity as NewsActivity).viewModel
setupRecyclerView()
newsAdapter.setOnItemClickListener {
val bundle = Bundle().apply {
putSerializable("article", it)
}
findNavController().navigate(
R.id.action_searchNewsFragment_to_articleFragment,
bundle
)
}
var job: Job? = null
etSearch.addTextChangedListener { editable ->
job?.cancel()
job = MainScope().launch {
delay(SEARCH_NEWS_TIME_DELAY)
editable?.let {
if(editable.toString().isNotEmpty()) {
viewModel.searchNews(editable.toString())
}
}
}
}
viewModel.searchNews.observe(viewLifecycleOwner, Observer { response ->
when(response) {
is Resource.Success -> {
hideProgressBar()
hideErrorMessage()
response.data?.let { newsResponse ->
newsAdapter.differ.submitList(newsResponse.articles.toList())
val totalPages = newsResponse.totalResults / Constants.QUERY_PAGE_SIZE + 2
isLastPage = viewModel.searchNewsPage == totalPages
if(isLastPage) {
rvSearchNews.setPadding(0, 0, 0, 0)
}
}
}
is Resource.Error -> {
hideProgressBar()
response.message?.let { message ->
Toast.makeText(activity, "An error occured: $message", Toast.LENGTH_LONG).show()
showErrorMessage(message)
}
}
is Resource.Loading -> {
showProgressBar()
}
}
})
btnRetry.setOnClickListener {
if (etSearch.text.toString().isNotEmpty()) {
viewModel.searchNews(etSearch.text.toString())
} else {
hideErrorMessage()
}
}
}
private fun hideProgressBar() {
paginationProgressBar.visibility = View.INVISIBLE
isLoading = false
}
private fun showProgressBar() {
paginationProgressBar.visibility = View.VISIBLE
isLoading = true
}
private fun hideErrorMessage() {
itemErrorMessage.visibility = View.INVISIBLE
isError = false
}
private fun showErrorMessage(message: String) {
itemErrorMessage.visibility = View.VISIBLE
tvErrorMessage.text = message
isError = true
}
var isError = false
var isLoading = false
var isLastPage = false
var isScrolling = false
val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
val isNoErrors = !isError
val isNotLoadingAndNotLastPage = !isLoading && !isLastPage
val isAtLastItem = firstVisibleItemPosition + visibleItemCount >= totalItemCount
val isNotAtBeginning = firstVisibleItemPosition >= 0
val isTotalMoreThanVisible = totalItemCount >= Constants.QUERY_PAGE_SIZE
val shouldPaginate = isNoErrors && isNotLoadingAndNotLastPage && isAtLastItem && isNotAtBeginning &&
isTotalMoreThanVisible && isScrolling
if(shouldPaginate) {
viewModel.searchNews(etSearch.text.toString())
isScrolling = false
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if(newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
isScrolling = true
}
}
}
private fun setupRecyclerView() {
newsAdapter = NewsAdapter()
rvSearchNews.apply {
adapter = newsAdapter
layoutManager = LinearLayoutManager(activity)
addOnScrollListener(this#SearchNewsFragment.scrollListener)
}
}
}`
SearchNewsAdapter
`class SearchNewsAdapter : RecyclerView.Adapter<SearchNewsAdapter.ArticleViewHolder>() {
// Inner class for viewHolder
inner class ArticleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
private val differCallback = object : DiffUtil.ItemCallback<Article>(){
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url== newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallback)
//recyclerViewFunction
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.search_article_preview,parent, false)
)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val article = differ.currentList[position]
holder.itemView.apply{
// Glide.with(this).load(article.urlToImage).into(ivArticleImage)
searchTitle.text = article.title
setOnClickListener{
onItemClickListener?.let{
it(article)
}
}
}
}
//item click listener to single article so that article fragment opens up the webview that shows our items
private var onItemClickListener: ((Article) -> Unit)? = null
fun setOnItemClickListener(listener:(Article) -> Unit){
onItemClickListener = listener
}
}`
NewsViewModel
`class NewsViewModel(
app: Application,
val newsRepository: NewsRepository
) : AndroidViewModel(app) {
val breakingNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
var breakingNewsPage = 1
var breakingNewsResponse: NewsResponse? = null
val searchNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
var searchNewsPage = 1
var searchNewsResponse: NewsResponse? = null
var newSearchQuery:String? = null
var oldSearchQuery:String? = null
init {
getBreakingNews("us")
}
fun getBreakingNews(countryCode: String) = viewModelScope.launch {
safeBreakingNewsCall(countryCode)
}
fun searchNews(searchQuery: String) = viewModelScope.launch {
safeSearchNewsCall(searchQuery)
}
private fun handleBreakingNewsResponse(response: Response<NewsResponse>) : Resource<NewsResponse> {
if(response.isSuccessful) {
response.body()?.let { resultResponse ->
breakingNewsPage++
if(breakingNewsResponse == null) {
breakingNewsResponse = resultResponse
} else {
val oldArticles = breakingNewsResponse?.articles
val newArticles = resultResponse.articles
oldArticles?.addAll(newArticles)
}
return Resource.Success(breakingNewsResponse ?: resultResponse)
}
}
return Resource.Error(response.message())
}
private fun handleSearchNewsResponse(response: Response<NewsResponse>) : Resource<NewsResponse> {
if(response.isSuccessful) {
response.body()?.let { resultResponse ->
if(searchNewsResponse == null || newSearchQuery != oldSearchQuery) {
searchNewsPage = 1
oldSearchQuery = newSearchQuery
searchNewsResponse = resultResponse
} else {
searchNewsPage++
val oldArticles = searchNewsResponse?.articles
val newArticles = resultResponse.articles
oldArticles?.addAll(newArticles)
}
return Resource.Success(searchNewsResponse ?: resultResponse)
}
}
return Resource.Error(response.message())
}
fun saveArticle(article: Article) = viewModelScope.launch {
newsRepository.upsert(article)
}
fun getSavedNews() = newsRepository.getSavedNews()
fun deleteArticle(article: Article) = viewModelScope.launch {
newsRepository.deleteArticle(article)
}
private suspend fun safeSearchNewsCall(searchQuery: String) {
newSearchQuery = searchQuery
searchNews.postValue(Resource.Loading())
try {
if(hasInternetConnection()) {
val response = newsRepository.searchNews(searchQuery, searchNewsPage)
searchNews.postValue(handleSearchNewsResponse(response))
} else {
searchNews.postValue(Resource.Error("No internet connection"))
}
} catch(t: Throwable) {
when(t) {
is IOException -> searchNews.postValue(Resource.Error("Network Failure"))
else -> searchNews.postValue(Resource.Error("Conversion Error"))
}
}
}
private suspend fun safeBreakingNewsCall(countryCode: String) {
breakingNews.postValue(Resource.Loading())
try {
if(hasInternetConnection()) {
val response = newsRepository.getBreakingNews(countryCode, breakingNewsPage)
breakingNews.postValue(handleBreakingNewsResponse(response))
} else {
breakingNews.postValue(Resource.Error("No internet connection"))
}
} catch(t: Throwable) {
when(t) {
is IOException -> breakingNews.postValue(Resource.Error("Network Failure"))
else -> breakingNews.postValue(Resource.Error("Conversion Error"))
}
}
}
private fun hasInternetConnection(): Boolean {
val connectivityManager = getApplication<NewsApplication>().getSystemService(
Context.CONNECTIVITY_SERVICE
) as ConnectivityManager
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val activeNetwork = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
return when {
capabilities.hasTransport(TRANSPORT_WIFI) -> true
capabilities.hasTransport(TRANSPORT_CELLULAR) -> true
capabilities.hasTransport(TRANSPORT_ETHERNET) -> true
else -> false
}
} else {
connectivityManager.activeNetworkInfo?.run {
return when(type) {
TYPE_WIFI -> true
TYPE_MOBILE -> true
TYPE_ETHERNET -> true
else -> false
}
}
}
return false
}
}
`
if(editable.toString().isNotEmpty()) { viewModel.searchNews(editable.toString()) }
this line of code prevent the empty query to be processed. So, when you delete everything from the edit text, it will do nothing, hence the result still the same.
even when I exit the search fragment and open it again,The recycler view remains stagnant instead of disappearing.
The search result is strored on NewsViewModel and because the ViewModel is initialized on the NewsActivity, it tied to the activity lifecycle. Even if you destroy the fragment, the search result (the whole ViewModel) will be kept because the activity is still alive. So, when you open back the search Fragment, the LiveData will give you the latest value.

Paging 3 Library calls the load method recursively with LoadType.APPEND

I am trying to display data from IconFinder API. It seems to be ItemKeyedDataSource for me and I used Paging3 to display the data as it's mentioned in the official docs.
Here is code, please check if there're any issues with the implementation I have done and where is the mistake.
IconSetsRemoteMediator
#OptIn(ExperimentalPagingApi::class)
class IconSetsRemoteMediator(
private val query: String?,
private val database: IconsFinderDatabase,
private val networkService: IconFinderAPIService
) : RemoteMediator<Int, IconSetsEntry>() {
private val TAG: String? = IconSetsRemoteMediator::class.simpleName
private val iconSetsDao = database.iconSetsDao
private val remoteKeysDao = database.remoteKeysDao
override suspend fun initialize(): InitializeAction {
// Load fresh data when ever the app is open new
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, IconSetsEntry>
): MediatorResult {
val iconSetID = when (loadType) {
LoadType.REFRESH -> {
null
}
LoadType.PREPEND -> {
return MediatorResult.Success(
endOfPaginationReached = true
)
}
LoadType.APPEND -> {
Log.d(TAG, "LoadType.APPEND")
val lastItem = state.lastItemOrNull()
if (lastItem == null) {
return MediatorResult.Success(
endOfPaginationReached = true
)
}
// Get the last item from the icon-sets list and return its ID
lastItem.iconset_id
}
}
try {
// Suspending network load via Retrofit.
val response = networkService.getAllPublicIconSets(after = iconSetID)
val iconSets = response.iconsets
val endOfPaginationReached = iconSets == null || iconSets.isEmpty()
database.withTransaction {
if (loadType == LoadType.REFRESH) {
// Delete the data in the database
iconSetsDao.deleteAllIconSets()
//remoteKeysDao.deleteRemoteKeys()
}
Log.d(TAG, "iconSets = ${iconSets?.size}")
Log.d(TAG, "endOfPaginationReached = $endOfPaginationReached")
Log.d(TAG, "state.anchorPosition = ${state.anchorPosition}")
Log.d(TAG, "state.pages = ${state.pages.size}")
val time = System.currentTimeMillis()
/*val remoteKeys = iconSets!!.map {
RemoteKeysEntry(it.iconset_id, time)
}*/
// Insert new IconSets data into database, which invalidates the current PagingData,
// allowing Paging to present the updates in the DB.
val data = iconSets!!.mapAsIconSetsEntry()
iconSetsDao.insertAllIconSets(data)
// Insert the remote key values which set the time at which the data is
// getting updated in the DB
//remoteKeysDao.insertRemoteKeys(remoteKeys)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: IOException) {
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
}
}
}
IconFinderRepository
class IconFinderRepository(
private val service: IconFinderAPIService,
private val database: IconsFinderDatabase
) {
private val TAG: String? = IconFinderRepository::class.simpleName
fun getPublicIconSets(): Flow<PagingData<IconSetsEntry>> {
Log.d(TAG, "New Icon Sets query")
val pagingSourceFactory = { database.iconSetsDao.getIconSets() }
#OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(pageSize = NUMBER_OF_ITEMS_TO_FETCH, enablePlaceholders = false),
remoteMediator = IconSetsRemoteMediator(
query = null,
database,
service
),
pagingSourceFactory = pagingSourceFactory
).flow
}
companion object {
const val NUMBER_OF_ITEMS_TO_FETCH = 20
}
}
IconSetViewHolder
class IconSetViewHolder private constructor(val binding: RecyclerItemIconSetBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(iconSetsEntry: IconSetsEntry?) {
if (iconSetsEntry == null) {
//Show the Loading UI
} else {
binding.model = iconSetsEntry
binding.executePendingBindings()
}
}
companion object {
fun from(parent: ViewGroup): IconSetViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = RecyclerItemIconSetBinding.inflate(layoutInflater, parent, false)
return IconSetViewHolder(binding)
}
}
}
IconSetAdapter
class IconSetAdapter : PagingDataAdapter<UiModel.IconSetDataItem, ViewHolder>(UI_MODEL_COMPARATOR) {
companion object {
private val UI_MODEL_COMPARATOR =
object : DiffUtil.ItemCallback<UiModel.IconSetDataItem>() {
override fun areContentsTheSame(
oldItem: UiModel.IconSetDataItem,
newItem: UiModel.IconSetDataItem
): Boolean {
return oldItem.iconSetsEntry.iconset_id == newItem.iconSetsEntry.iconset_id
}
override fun areItemsTheSame(
oldItem: UiModel.IconSetDataItem,
newItem: UiModel.IconSetDataItem
): Boolean =
oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return if (viewType == R.layout.recycler_item_icon_set) {
IconSetViewHolder.from(parent)
} else {
IconSetViewHolder.from(parent)
}
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is UiModel.IconSetDataItem -> R.layout.recycler_item_icon_set
null -> throw UnsupportedOperationException("Unknown view")
else -> throw UnsupportedOperationException("Unknown view")
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val uiModel = getItem(position)
uiModel.let {
when (uiModel) {
is UiModel.IconSetDataItem -> (holder as IconSetViewHolder).bind(uiModel.iconSetsEntry)
}
}
}
}
HomeFragmentViewModel
class HomeFragmentViewModel(application: Application) : AndroidViewModel(application) {
private val TAG: String? = HomeFragmentViewModel::class.simpleName
private val repository: IconFinderRepository = IconFinderRepository(
IconFinderAPIService.create(),
IconsFinderDatabase.getInstance(application)
)
private var iconSetsQueryResult: Flow<PagingData<UiModel.IconSetDataItem>>? = null
fun iconSetsQuery(): Flow<PagingData<UiModel.IconSetDataItem>> {
val newResult: Flow<PagingData<UiModel.IconSetDataItem>> = repository.getPublicIconSets()
.map { pagingData -> pagingData.map { UiModel.IconSetDataItem(it) } }
.cachedIn(viewModelScope)
iconSetsQueryResult = newResult
return newResult
}
/**
* Factory for constructing HomeFragmentViewModel
*/
class Factory(private val application: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(HomeFragmentViewModel::class.java)) {
return HomeFragmentViewModel(application) as T
}
throw IllegalArgumentException("Unable to construct ViewModel")
}
}
}
sealed class UiModel {
data class IconSetDataItem(val iconSetsEntry: IconSetsEntry) : UiModel()
}
IconSetFragment: This is one of the fragments implemented as part of ViewPager. Its parent is a Fragment in an Activity.
class IconSetFragment : Fragment() {
private val TAG: String = IconSetFragment::class.java.simpleName
/**
* Declaring the UI Components
*/
private lateinit var binding: FragmentIconSetBinding
private val viewModel: HomeFragmentViewModel by viewModels()
private val adapter = IconSetAdapter()
private var job: Job? = null
companion object {
fun newInstance(): IconSetFragment {
return IconSetFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Get a reference to the binding object
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_icon_set, container, false)
Log.d(TAG, "onCreateView")
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initAdapter()
job?.cancel()
job = viewLifecycleOwner.lifecycleScope.launch {
viewModel.iconSetsQuery().collectLatest {
adapter.submitData(it)
Log.d(TAG, "collectLatest $it")
}
}
}
private fun initAdapter() {
binding.rvIconSetList.adapter = adapter
/*.withLoadStateHeaderAndFooter(
header = LoadStateAdapter(), // { adapter.retry() },
footer = LoadStateAdapter { adapter.retry() }
)*/
}
}
IconSetsDao
#Dao
interface IconSetsDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllIconSets(iconSets: List<IconSetsEntry>)
#Query("SELECT * FROM icon_sets_table")
fun getIconSets(): PagingSource<Int, IconSetsEntry>
#Query("DELETE FROM icon_sets_table")
suspend fun deleteAllIconSets()
}
This is the Logcat screenshot, the load() method is being invoked without any scrolling action.
I have the similar issue, seems the recursive loading issue is fixed by setting the recyclerView.setHasFixedSize(true)

Android Paging 3.0 load method not working

i am trying to implement the android paging 3.0 library and i am having an issue with the .load method not working, at first i thought the adapter submitData or layoutManager was not working cause i did get response from LoadResult.Page, but now when looking further it kind of stopped working at all
here's my code
1- Fragment
private fun initAdapter() {
searchAdapter = SearchItemsAdapter {
//item clicked
}
val gridLayoutManager = GridLayoutManager(requireContext(), 2)
binding?.layoutManager = gridLayoutManager
binding?.searchAdapter = searchAdapter
}
private fun startSearch() {
searchJob?.cancel()
searchJob = viewLifecycleOwner.lifecycleScope.launch {
searchViewModel?.getSearchResults("phone")?.collectLatest {
searchAdapter?.submitData(it)
}
}
}
2- ViewModel
fun getSearchResults(query: String): Flow<PagingData<ItemModel>> {
val lastResult = currentSearchResult
if (query == currentQueryValue && lastResult != null) {
return lastResult
}
currentQueryValue = query
val newResult: Flow<PagingData<ItemModel>> = searchRepository.getSearchResults(query)
.cachedIn(viewModelScope)
currentSearchResult = newResult
return newResult
}
3- SearchRepository
fun getSearchResults(query: String): Flow<PagingData<ItemModel>> {
return Pager(
config = PagingConfig(
pageSize = 10
),
pagingSourceFactory = { SearchPagingSource(client, query) }
).flow
}
4- PagingSource
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ItemModel> {
val position: Int = params.key ?: 1
return try {
val response: SearchResponse = service.getSearchResult(query, position)
val items = response.metadata.results
val nextKey = if (items.isEmpty()) {
null
} else {
position + 1
}
LoadResult.Page(
data = items,
prevKey = if (position == 1) null else position - 1,
nextKey = nextKey
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
}
}
5- Adapter
override fun onBindViewHolder(holder: SearchItemsViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.item = item
holder.itemView.setOnClickListener {
itemClickCallback(item)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchItemsViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = SearchItemLayoutBinding.inflate(layoutInflater, parent, false)
return SearchItemsViewHolder(binding)
}
companion object {
private val diffCallBack = object : DiffUtil.ItemCallback<ItemModel>() {
override fun areItemsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean =
oldItem == newItem
}
}
i found the issue that i had
1- i was not calling the startSearch method from the onViewCreated method of the fragment, that didn't help with the API calling
2- the recyclerView in the XML had wrap_content instead of 0dp as height
For me, my data class was
data class Score(
val userId: String,
val username: String,
val avatarPath: String,
val score: Long,
)
I changed it to
data class Score(
var userId: String? = null,
var username: String? = null,
var avatarPath: String? = null,
var score: Long? = null,
)
and now it works !!

android: kotlin: Data not loading in adapter while implementing paging 3

I was implementing paging 3 in my app.
I tried to follow google codelabs and overview on paging 3 from documentation
However it fails to display the data in the adapter, I put logs in the adapter and no logs from adapter show up on logcat.
I tried to debug the codelabs code and data flow was exactly as mine, since I can't( or don't know yet) how to intervene paging which is received in flow in fragment, I am unable to tell if my fragment receives the data. Also I have never used adapter.submitdata() and new to coroutines, I am not sure if it could be the scope which is the problem.
Any help is appreciated. Thanks in advance.
Have a nice day.
fragment
transactionsAdapterPaging = TransactionsPagedListAdapter()
homeFragmentDataBinding.transactionsRV.apply {
layoutManager = LinearLayoutManager(requireContext())
setHasFixedSize(true)
adapter = transactionsAdapterPaging
}
homeFragmentDataBinding.transactionsRV.adapter = transactionsAdapterPaging
lifecycleScope.launch {
mainViewModel.getDataFromRemote().collectLatest {
Log.e(TAG, "initializeTransactionAdapter: ++++++++====== " + it.toString())
transactionsAdapterPaging.submitData(it)
}
}
viewmodel
private var currentSearchResult: Flow<PagingData<TransactionDetail>>? = null
fun getDataFromRemote(): Flow<PagingData<TransactionDetail>> {
val lastResult = currentSearchResult
val newResult: Flow<PagingData<TransactionDetail>> = mainRepository.getDataFromRemote()
.cachedIn(viewModelScope)
currentSearchResult = newResult
return newResult
}
repository
fun getDataFromRemote(): Flow<PagingData<TransactionDetail>> {
return Pager(
config = PagingConfig(pageSize = 1, enablePlaceholders = false),
pagingSourceFactory = { TransactionsDataSource() }
).flow
}
data source
class TransactionsDataSource() : PagingSource<Int, TransactionDetail>() {
private val TAG = "UserDataSource"
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TransactionDetail> {
val position = params.key ?: 1
return try {
lateinit var transactionDetail: List<TransactionDetail>
var response: Response<Any?>? = NewAPIServiceClient.getAllTransactionsPaging(
Application.getInstance(),
Prefs.getString(Constants.CARD_UUID_IN_USE, ""),
position,
null
)
Log.e(TAG, "load: " + response)
if (response != null && response.isSuccessful) {
if (response?.body() != null) {
val gson = Gson()
try {
val jsonObject1 = JSONObject(gson.toJson(response?.body()))
val transactionsGson = jsonObject1.getJSONArray(Constants.PN_DATA_NEW)
transactionDetail = gson.fromJson(
transactionsGson.toString(),
object : TypeToken<List<TransactionDetail>?>() {}.type
)
} catch (e:Exception){
}
}
}
val repos = transactionDetail
Log.e(TAG, "load: ===== " + repos.toString() )
return LoadResult.Page(
data = repos,
prevKey = if (position == 1) null else position - 1,
nextKey = if (repos.isEmpty()) null else position + 1
)
} catch (exception: IOException) {
Log.e(TAG, "load: 11111111111111111 " + exception )
LoadResult.Error(exception)
} catch (exception: HttpException) {
Log.e(TAG, "load: 11111111111111111 " + exception )
LoadResult.Error(exception)
}
}
}
adapter
class TransactionsPagedListAdapter() :
PagingDataAdapter<TransactionDetail, RepoViewHolder>(
DIFF_CALLBACK
) {
private val TAG = "AdapterPaging"
private var serviceListFiltered: List<TransactionDetail>? = null
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RepoViewHolder {
Log.e(TAG, "onCreateViewHolder: ")
val itemBinding: RvListItemTransactionsHomeBinding =
RvListItemTransactionsHomeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return RepoViewHolder(itemBinding)
}
override fun onBindViewHolder(
holder: RepoViewHolder,
position: Int
) {
Log.e(TAG, "onBindViewHolder: ")
val repoItem = getItem(position)
if (repoItem != null) {
(holder).bind(repoItem, position)
}
}
override fun getItemCount(): Int {
return if (serviceListFiltered == null) {
0
} else serviceListFiltered!!.size
}
companion object {
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<TransactionDetail>() {
override fun areItemsTheSame(oldItem: TransactionDetail, newItem: TransactionDetail) =
oldItem.uuid == newItem.uuid
override fun areContentsTheSame(
oldItem: TransactionDetail,
newItem: TransactionDetail
) =
oldItem == newItem
}
}
}
class RepoViewHolder internal constructor(itemBinding: RvListItemTransactionsHomeBinding) :
RecyclerView.ViewHolder(itemBinding.getRoot()), View.OnClickListener {
private val mDataBinding: RvListItemTransactionsHomeBinding = itemBinding
var rootView: View
fun bind(invoice: TransactionDetail, position: Int) {
rootView.transaction_text_title.text = invoice.merchant?.merchantName
var amountWithSymbol =
Utilities.getFormattedAmount(
Application.getInstance()?.applicationContext,
invoice.amount.toString()
)
rootView.transaction_amount.text = amountWithSymbol
rootView.transactions_date.text = invoice.timestamp
}
override fun onClick(view: View) {
if (adapterPosition > RecyclerView.NO_POSITION) {
}
}
init {
val itemView: View = itemBinding.getRoot()
rootView = mDataBinding.constraintContainer
itemView.setOnClickListener(this)
}
}
`override fun getItemCount(): Int {
return if (serviceListFiltered == null) {
0
} else serviceListFiltered!!.size
}`
Don't override getItemCount in your recyclerview adapter.

Categories

Resources