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 !!
Related
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
In this app, I tried to use Paging3, Retrofit, and RXJava to get a list of popular movies, this is the link of API, the problem is in MoviesDataAdapter class when I didn't override getItemCount fun the recyclerView didn't show anything but the result showing in Log when I return item count, the AS shows warning notify on this method
java.lang.StackOverflowError: stack size 8192KB
at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
2022-06-10 22:48:58.080 10543-10543/com.mml.moviemvvm E/AndroidRuntime: at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
let's start with model classes Movie
data class Movie(
val id: Int,
#SerializedName("poster_path")
val posterPath: String,
#SerializedName("release_date")
val releaseDate: String,
val title: String
)
and MovieResponse
data class MovieResponse(
val page: Int,
#SerializedName("results")
val movieList: List<Movie>,
#SerializedName("total_pages")
val totalPages: Int,
#SerializedName("total_results")
val totalResults: Int
)
enum Status & NetworkState class
enum class Status {
LOADING,
SUCCESS,
FAILED
}
class NetworkState(val status: Status, val msg: String) {
companion object {
val SUCCEED: NetworkState = NetworkState(Status.SUCCESS, "Success")
val LOADING: NetworkState = NetworkState(Status.LOADING, "Running")
val ERROR: NetworkState = NetworkState(Status.FAILED, "Something went wrong")
val ENDOFLIST = NetworkState(Status.FAILED, "You have reached the end")
}
}
TheMovieDbApi interface
interface TheMovieDbApi {
#GET("movie/popular")
fun getPopularMovies(#Query("page") page: Int): Single<MovieResponse>
#GET("movie/{movie_id}")
fun getMovieDetails(#Path("movie_id") id: Int): Single<MovieDetails>
}
TheMovieClient
const val API_KEY = "some key"
const val BASE_URL = "https://api.themoviedb.org/3/"
const val POSTER_BASE_URL = "https://image.tmdb.org/t/p/w342"
const val FIRST_PAGE = 1
const val POST_PER_PAGE = 20
object TheMovieDBClient {
fun getClient(): TheMovieDbApi {
val requestInterceptor = Interceptor { chain ->
// Interceptor take only one argument which is a lambda function so parenthesis can be omitted
val url = chain.request()
.url()
.newBuilder()
.addQueryParameter("api_key", API_KEY)
.build()
val request = chain.request()
.newBuilder()
.url(url)
.build()
return#Interceptor chain.proceed(request) //explicitly return a value from whit # annotation. lambda always returns the value of the last expression implicitly
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(requestInterceptor)
.connectTimeout(60, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(BASE_URL)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(TheMovieDbApi::class.java)
}
}
MoviesPagingSource class
class MoviesPagingSource(
private val apiService: TheMovieDbApi,
) : PagingSource<Int, Movie>() {
override fun getRefreshKey(state: PagingState<Int, Movie>): Int? {
TODO("Not yet implemented")
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> {
val position = params.key ?: FIRST_PAGE
return try {
var movieResponse = MovieResponse(3,
listOf(),1,1)
apiService.getPopularMovies(position)
.subscribeOn(Schedulers.io())
.subscribe(
{
if(it != null){
movieResponse = it
Log.e(TAG, "load: $it" )
}
}, { throwable ->
Log.e(TAG, "load: ${throwable.cause?.message}")
}
)
LoadResult.Page(
data = movieResponse.movieList,
prevKey = if (position == FIRST_PAGE) null else
position - 1,
nextKey = if (movieResponse.movieList.isEmpty()) null else position + 1
)
} catch (ex: IOException) {
LoadResult.Error(ex)
} catch (ex: HttpException) {
LoadResult.Error(ex)
}
}
}
MoviesLoadStateAdapter
class MoviesLoadStateAdapter(private val retry: () -> Unit) :
LoadStateAdapter<MoviesLoadStateAdapter.LoadStateViewHolder>() {
inner class LoadStateViewHolder(private val binding: MoviesLoadStateFooterBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
binding.buttonRetry.setOnClickListener {
retry.invoke()
}
}
fun bind(loadState: LoadState) {
binding.apply {
progressBar.isVisible = loadState is LoadState.Loading
buttonRetry.isVisible = loadState !is LoadState.Loading
textViewError.isVisible = loadState !is LoadState.Loading
}
}
}
override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {
holder.bind(loadState)
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder {
val binding: MoviesLoadStateFooterBinding = MoviesLoadStateFooterBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return LoadStateViewHolder(binding)
}
}
MoviesReposory class
class MoviesRepository(private val api: TheMovieDbApi) {
fun getMoviesResults() =
Pager(
config = PagingConfig(
pageSize = 20,
maxSize = 100,
enablePlaceholders = false,
),
pagingSourceFactory = { MoviesPagingSource(api) }
).liveData
}
MovieDataAdapter
class MoviesDataAdapter(private val listener: MainActivity) :
PagingDataAdapter<Movie, MoviesDataAdapter.MovieItemViewHolder>(MOVIE_COMPARATOR) {
override fun onBindViewHolder(holder: MovieItemViewHolder, position: Int) {
val movie = getItem(position)
holder.bind(movie)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieItemViewHolder {
val binding = MovieListItemBinding.inflate(
LayoutInflater.from(parent.context),
parent, false
)
return MovieItemViewHolder(binding)
}
override fun getItemCount(): Int {
return itemCount
}
companion object {
private val MOVIE_COMPARATOR = object : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Movie, newItem: Movie) =
oldItem == newItem
}
}
interface OnClickListener{
fun onItemClick(movie: Movie?)
}
inner class MovieItemViewHolder(private val binding: MovieListItemBinding) :
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(movie: Movie?) {
binding.apply {
cvMovieTitle.text = movie?.title
cvMovieReleaseDate.text = movie?.releaseDate
val moviePosterURL = POSTER_BASE_URL + movie?.posterPath
Glide.with(cardView.context)
.load(moviePosterURL)
.into(cvIvMoviePoster)
}
}
}
}
when I run the app without overriding getItemCount fun it's showing no result but it printed in the log
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)
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())
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.