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.
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
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.
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 !!
The Elements of the project that don't work
And I check if data is no null and do default submitList in the fragment.
Btw here is the link to the documentation
SearchPagingSource
These logs aren't even shown
class SearchPagingSource(
private val api: Api,
private val query: String
) : PagingSource<Int, Image>
() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Image> {
val position = params.key ?: 0
Log.d("SUPERTAG", "response")
return try {
val response =
api.search(query, position, params.loadSize, Locale.getDefault().language)
val list = ArrayList<Image>()
response.gif.forEach {
list.add(it.image)
}
Log.d("SUPERTAG", "response: $list")
LoadResult.Page(
data = list,
prevKey = null,
nextKey = if (list.isEmpty()) null else position + 15
)
} catch (e: IOException) {
// no connection
Log.d("SUPERTAG", "IOException: ${e.message}")
LoadResult.Error(e)
} catch (e: HttpException) {
// error loading
Log.d("SUPERTAG", "HttpException: ${e.message}")
LoadResult.Error(e)
}
}
}
ViewModel
Null because of the null that returned by the repository.
class SearchViewModel : ViewModel() {
private val searchRepository = SearchRepository.getInstance()
private val _query = MutableLiveData<String>()
private val _results = _query.map { data ->
searchRepository.search(data).value
}
val results = _results
private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error
#SuppressLint("StaticFieldLeak")
private lateinit var progressBar: ProgressBar
fun initProgress(progress: ProgressBar) {
progressBar = progress
}
fun search(query: String, errorLoading: String) {
viewModelScope.launch {
try {
progressBar.visibility = View.VISIBLE
_query.value = query
Log.d("SUPERTAG", "result2: ${searchRepository.search(_query.value!!).value}")
progressBar.visibility = View.GONE
} catch (e: Exception) {
_error.value = e.message
}
}
}
}
Repository
Exactly this part of the code returns null, I checked It by logs. I guess I do smth wrong with parameters or in general.
object SearchRepository {
private lateinit var instance: SearchRepository
private val app: App by lazy {
App().getInstance()
}
fun getInstance(): SearchRepository {
instance = this
app.initRetrofit()
return instance
}
fun search(query: String) = Pager(
config = PagingConfig(
15,
maxSize = 50,
enablePlaceholders = false
),
pagingSourceFactory = {
SearchPagingSource(app.api, query)
}
).liveData
}
If I do like this, I get at least snackbar and an error. Usually it shows nothing and even no progressBar.
So, If I add jumpThreshold = 0, I get a snackbar with an error that I don't have usually.
fun search(query: String) = Pager(
config = PagingConfig(
pageSize = 15,
jumpThreshold = 0
),
pagingSourceFactory = {
SearchPagingSource(app.api, query)
}
).liveData
Edit
So, I did it with flow and it works a bit, but Im still not getting a list in my recycler.
Repository
fun getListData(query: String): Flow<PagingData<Image>> {
return Pager(
config = PagingConfig(
pageSize = 15,
maxSize = 50,
enablePlaceholders = true
), pagingSourceFactory = {
SearchPagingSource(query = query, api = app.api)
}).flow
}
ViewModel
fun search(query: String){
viewModelScope.launch {
try {
searchRepository.getListData(query).collectLatest {
it.map {
Log.d("SUPERTAG", "image $it")
}
Log.d("SUPERTAG", it.toString())
_results.value = it
}
} catch (e: Exception){
_error.value = e.message
}
}
}
The answer was in the adapter!
class SearchAdapter(
diffCallback: DiffUtil.ItemCallback<Image>,
private val clickListener: (url: String) -> Unit
) : PagingDataAdapter<Image, SearchAdapterViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchAdapterViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.list_item, parent, false)
return SearchAdapterViewHolder(view)
}
override fun onBindViewHolder(holder: SearchAdapterViewHolder, position: Int) {
val item = getItem(position)
if(item != null){
holder.imageView.setOnClickListener {
clickListener(item.original.url)
}
holder.bind(item)
}
}
}
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())