I am trying to implement a paging logic with partial data loading per one page. I have a page with 100 max page_size value but I don't need all 100 values per one request, what I need is to request 30 records in one request, 30 in second, 30 in third 10 (because of max 100) in fourth request and after that I change page number. That how I wrote this now:
Data source class
class NewsDataSource(
private val compositeDisposable: CompositeDisposable,
private val newsRequests: NewsRequests
) : PageKeyedDataSource<PageNewsKey, News.Data?>() {
private val pRequestNewsStatuses = MutableLiveData<LiveDataStatuses>(LiveDataStatuses.IDLE)
val requestNewsStatuses: LiveData<LiveDataStatuses> = pRequestNewsStatuses
override fun loadInitial(
params: LoadInitialParams<PageNewsKey>,
callback: LoadInitialCallback<PageNewsKey, News.Data?>
) {
val pageNewsKey = PageNewsKey()
pRequestNewsStatuses.postValue(LiveDataStatuses.WAITING)
compositeDisposable.add(
newsRequests.getNews(pageNewsKey.newsCount, pageNewsKey.page)
.subscribeOn(Schedulers.io())
.map {
it.data ?: emptyList()
}
.subscribe({
callback.onResult(it, null, pageNewsKey.nextPageKey)
}, {
})
)
}
override fun loadBefore(
params: LoadParams<PageNewsKey>,
callback: LoadCallback<PageNewsKey, News.Data?>
) {
}
override fun loadAfter(
params: LoadParams<PageNewsKey>,
callback: LoadCallback<PageNewsKey, News.Data?>
) {
pRequestNewsStatuses.postValue(LiveDataStatuses.WAITING)
compositeDisposable.add(
newsRequests.getNews(params.key.newsCount, params.key.page)
.subscribeOn(Schedulers.io())
.map {
it.data ?: emptyList()
}
.subscribe({
callback.onResult(it, params.key.nextPageKey)
}, {
})
)
}
Key class:
class PageNewsKey {
var newsCount: Int = NEWS_COUNT_DEFAULT_VALUE
var page: Int = 1
val nextPageKey: PageNewsKey
get() {
return iterate()
}
private fun iterate(): PageNewsKey {
if (newsCount == NEWS_COUNT_MAX_VALUE) {
newsCount = NEWS_COUNT_DEFAULT_VALUE
page = page.inc()
} else {
val newNewsCount = newsCount + NEWS_COUNT_DEFAULT_VALUE
newsCount = if (newNewsCount > NEWS_COUNT_MAX_VALUE) {
NEWS_COUNT_MAX_VALUE
} else {
newNewsCount
}
}
return this
}
}
Data source factory:
class NewsDataSourceFactory(
private val compositeDisposable: CompositeDisposable,
private val newsRequests: NewsRequests
) : DataSource.Factory<PageNewsKey, News.Data?>() {
private val pNewsDataSource = MutableLiveData<NewsDataSource?>(null)
val newsDataSource: LiveData<NewsDataSource?> = pNewsDataSource
override fun create(): DataSource<PageNewsKey, News.Data?> {
val newsDataSource = NewsDataSource(compositeDisposable, newsRequests)
pNewsDataSource.postValue(newsDataSource)
return newsDataSource
}
fun refresh() {
pNewsDataSource.value?.invalidate()
}
}
This code is in view model class:
private val newsDataSourceFactory = NewsDataSourceFactory(compositeDisposable, NewsRequests.getNewsRequest())
private val newsDataSourceFactoryConfig = PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setPageSize(NEWS_COUNT_DEFAULT_VALUE)
.build()
val news: LiveData<PagedList<News.Data?>> = LivePagedListBuilder(newsDataSourceFactory, newsDataSourceFactoryConfig).build()
Live data observer inside desired fragment:
homeViewModel.news.observe(this, Observer {
it?.let {
newsAdapter.submitList(it)
}
})
Adapter:
class NewsAdapter : PagedListAdapter<News.Data, NewsAdapter.ViewHolder>(News.Data.NEWS_DATA_DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.news_row, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bingView(getItem(position))
}
fun getItemAtPosition(position: Int): News.Data? = getItem(position)
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private var iv_news_image: ImageView? = null
private var tv_news_title: TextView? = null
private var tv_news_subtitle: TextView? = null
private var clpb_news_image: ContentLoadingProgressBar? = null
init {
iv_news_image = itemView.findViewById(R.id.iv_news_image)
tv_news_title = itemView.findViewById(R.id.tv_news_title)
tv_news_subtitle = itemView.findViewById(R.id.tv_news_subtitle)
clpb_news_image = itemView.findViewById(R.id.clpb_news_image)
clpb_news_image?.hide()
}
fun bingView(data: News.Data?) {
iv_news_image?.let {
clpb_news_image?.show()
GlideApp.with(itemView.context)
.load(data?.imageUrl)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transition(DrawableTransitionOptions.withCrossFade(500))
.listener(object : IDoAfterTerminateGlide {
override fun doAfterTerminate() {
clpb_news_image?.hide()
}
})
.dontAnimate()
.into(it)
}
tv_news_title?.text = data?.title
tv_news_subtitle?.text = data?.subtitle
}
}
}
And finally data class with diff callback:
data class News(
#SerializedName("current_page")
val currentPage: Int? = null,
#SerializedName("data")
val data: List<Data?>? = null,
#SerializedName("first_page_url")
val firstPageUrl: String? = null,
#SerializedName("from")
val from: Int? = null,
#SerializedName("last_page")
val lastPage: Int? = null,
#SerializedName("last_page_url")
val lastPageUrl: String? = null,
#SerializedName("next_page_url")
val nextPageUrl: String? = null,
#SerializedName("path")
val path: String? = null,
#SerializedName("per_page")
val perPage: String? = null,
#SerializedName("prev_page_url")
val prevPageUrl: String? = null,
#SerializedName("to")
val to: Int? = null,
#SerializedName("total")
val total: Int? = null
) {
data class Data(
#SerializedName("body")
val body: String? = null,
#SerializedName("date")
val date: String? = null,
#SerializedName("image_url")
val imageUrl: String? = null,
#SerializedName("news_id")
val newsId: String? = null,
#SerializedName("subtitle")
val subtitle: String? = null,
#SerializedName("title")
val title: String? = null
) {
companion object {
val NEWS_DATA_DIFF_CALLBACK = object : DiffUtil.ItemCallback<Data>() {
override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.newsId == newItem.newsId
}
override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.body == newItem.body && oldItem.date == oldItem.date
&& oldItem.imageUrl == newItem.imageUrl
&& oldItem.subtitle == newItem.subtitle
&& oldItem.title == newItem.title
}
}
}
}
}
json response
And this setup is working as I expected, it makes requests:
www.example.com?page=1&page_size=30
www.example.com?page=1&page_size=60
www.example.com?page=1&page_size=90
www.example.com?page=1&page_size=10
www.example.com?page=2&page_size=30
But the problem is that diff item callback doesn't make it's work as I am expecting, and I receive duplicated of data. So my question is pretty simple, does android page keyed data source provide such functionality?
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
So i am trying 2 things here and it seems to be all right but i am not beeing able to reach my API response. First i want to Filter my API to All information, only Images and only Pdf.- and second to make a recyclerView OnItemClickListener. If you can help to provide me this error i will very graceful. Thank You .
//Main Activity
class MainActivity : AppCompatActivity() {
private lateinit var recyclerview_users: RecyclerView
private lateinit var imageList : ArrayList<ImageModel>
private lateinit var imageAdapter: ImageAdapter
private var imageModel: List<ImageModel> = mutableListOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val allButton = findViewById<Button>(R.id.button_all)
allButton.setOnClickListener {
getBlob()
}
val photoButton = findViewById<Button>(R.id.button_image)
photoButton.setOnClickListener {
filterImageModel("photo")
}
val pdfButton = findViewById<Button>(R.id.button_pdf)
pdfButton.setOnClickListener {
filterImageModel("pdf")
}
recyclerview_users = findViewById(R.id.recycler_view)
recyclerview_users.setHasFixedSize(true)
recyclerview_users.layoutManager = LinearLayoutManager(this)
imageList = ArrayList()
imageAdapter = ImageAdapter(imageModel)
recyclerview_users.adapter = imageAdapter
var layoutManager = GridLayoutManager(this, 3)
recyclerview_users.layoutManager = layoutManager
getBlob()
}
private fun getBlob(){
val plcApi = ServiceGenerator.buildService(PLCApi::class.java)
val call = plcApi.getBlob()
call.enqueue(object : Callback<MutableList<ImageModel>>{
override fun onResponse(
call: Call<MutableList<ImageModel>>,
response: Response<MutableList<ImageModel>>
){
if (response.isSuccessful){
imageModel = response.body()!!
imageAdapter.updateImageModel(imageModel)
val imageList = response.body()!! as ArrayList<ImageModel>
val imageAdapter = ImageAdapter(imageList)
recyclerview_users.adapter = imageAdapter
imageAdapter.setOnitemClickListener { imageModel ->
val intent = Intent(this#MainActivity, ImageInformation::class.java)
startActivity(intent)
}
}else{
Toast.makeText(this#MainActivity, "Failed to retrieve data, please try again", Toast.LENGTH_SHORT).show()
}
}
override fun onFailure(call: Call<MutableList<ImageModel>>, t: Throwable){
Toast.makeText(this#MainActivity, "Network error, please check your connection", Toast.LENGTH_SHORT).show()
}
})
}
private fun filterImageModel(contentType: String) {
val filteredImageModel = imageModel.filter {
it.contentType == contentType
}
imageModel = filteredImageModel
imageAdapter.updateImageModel(filteredImageModel)
}
}
//ImageAdapter
class ImageAdapter(private var imageModel: List<ImageModel>) : RecyclerView.Adapter<ImageAdapter.ImageViewHolder>() {
private var onItemClickListener: ((ImageModel) -> Unit)? = null
fun setOnitemClickListener(listener: (ImageModel) -> Unit){
onItemClickListener = listener
}
fun updateImageModel(newImageModel: List<ImageModel>){
imageModel = newImageModel
notifyDataSetChanged()
}
class ImageViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
var downloadUrl: TextView = itemView.findViewById(R.id.downloadUrl)
var previewUrl: ImageView = itemView.findViewById(R.id.previewUrl)
var id: TextView = itemView.findViewById(R.id.id)
var filename: TextView = itemView.findViewById(R.id.fileName)
var filesize: TextView = itemView.findViewById(R.id.filesize)
var contentType: TextView = itemView.findViewById(R.id.contentType)
var createdBy: TextView = itemView.findViewById(R.id.createdBy)
var createdTimestamp: TextView = itemView.findViewById(R.id.createdTimestamp)
var creationSource: TextView = itemView.findViewById(R.id.creationSource)
var domainIdentityType: TextView = itemView.findViewById(R.id.domainIdentityType)
var domainIdentityValue: TextView = itemView.findViewById(R.id.domainIdentityValue)
var tags: TextView = itemView.findViewById(R.id.tags)
var description: TextView = itemView.findViewById(R.id.description)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.layout_items, parent, false)
return ImageViewHolder(view)
}
#SuppressLint("CheckResult", "ResourceType")
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val imageId = imageModel[position]
holder.downloadUrl.text = imageId.downloadUrl
if (imageId.contentType!!.startsWith("image")){
Glide.with(holder.itemView.context).load(imageId.previewUrl).into(holder.previewUrl)
}else{
holder.previewUrl.setImageResource(R.drawable.pdf)
}
holder.id.text = imageId.id
holder.filename.text = imageId.fileName
holder.filesize.text = imageId.fileName.toString()
holder.contentType.text = imageId.contentType
holder.createdBy.text = imageId.createdBy
holder.createdTimestamp.text = imageId.createdTimestamp
holder.creationSource.text = imageId.creationSource
holder.domainIdentityType.text = imageId.domainIdentityType
holder.domainIdentityValue.text = imageId.domainIdentityValue
holder.tags.text = imageId.tags.toString()
holder.description.text = imageId.description
holder.itemView.setOnClickListener{
onItemClickListener?.invoke(imageId)
}
}
override fun getItemCount(): Int {
return imageModel.size
}
}
//ImageModel
data class ImageModel(
#SerializedName("downloadUrl")
var downloadUrl: String? = null,
#SerializedName("previewUrl")
val previewUrl: String? = null,
#SerializedName("id")
var id: String? = null,
#SerializedName("fileName")
val fileName: String? = null,
#SerializedName("filesize")
val filesize: Int,
#SerializedName("contentType")
val contentType: String? = null,
#SerializedName("createdBy")
val createdBy: String? = null,
#SerializedName("createdTimestamp")
val createdTimestamp: String? = null,
#SerializedName("creationSource")
val creationSource: String? = null,
#SerializedName("domainIdentityType")
val domainIdentityType: String? = null,
#SerializedName("domainIdentityValue")
val domainIdentityValue: String? = null,
#SerializedName("tags")
val tags: List<String>? = null,
#SerializedName("description")
val description: String? = null)
: Parcelable{
constructor(parcel: Parcel) : this(
parcel.readString()!!,
parcel.readString()!!,
parcel.readString()!!,
parcel.readString()!!,
parcel.readInt(),
parcel.readString()!!,
parcel.readString()!!,
parcel.readString()!!,
parcel.readString()!!,
parcel.readString()!!,
parcel.readString()!!,
parcel.createStringArrayList(),
parcel.readString()!!
)
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(downloadUrl)
parcel.writeString(previewUrl)
parcel.writeString(id)
parcel.writeString(fileName)
parcel.writeInt(filesize)
parcel.writeString(contentType)
parcel.writeString(createdBy)
parcel.writeString(createdTimestamp)
parcel.writeString(creationSource)
parcel.writeString(domainIdentityType)
parcel.writeString(domainIdentityValue)
parcel.writeString(tags.toString())
parcel.writeString(description)
}
companion object CREATOR : Parcelable.Creator<ImageModel> {
override fun createFromParcel(parcel: Parcel): ImageModel {
return ImageModel(parcel)
}
override fun newArray(size: Int): Array<ImageModel?> {
return arrayOfNulls(size)
}
}
}
//PLC API
interface PLCApi {
#GET("/BlobStorage/Blobs?StorageContainerId=ccef4ed7-7ec7-421d-8afb-3dce0a8be39c")
fun getBlob():Call<MutableList<ImageModel>>
}
//ServiceGenerator
object ServiceGenerator {
private val client = OkHttpClient.Builder().build()
private val retrofit = Retrofit.Builder()
.baseUrl("http://app-bbg-blob-assessment.azurewebsites.net/")
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
fun <T> buildService(service: Class<T>): T {
return retrofit.create(service)
}
}
//ImageInformation where i want to invoke the ClickListener
class ImageInformation : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_image_information)
val image = intent.getParcelableExtra<ImageModel>("ImageModel")
if (image != null){
val downloadUrl: TextView = findViewById(R.id.downloadUrla)
val previewUrl : ImageView = findViewById(R.id.previewUrla)
val id: TextView = findViewById(R.id.idsa)
val filename: TextView = findViewById(R.id.fileNamea)
val filesize: TextView = findViewById(R.id.filesizea)
val contentType: TextView = findViewById(R.id.contentTypea)
val createdBy: TextView = findViewById(R.id.createdBya)
val createdTimestamp: TextView = findViewById(R.id.createdTimestampa)
val creationSource: TextView = findViewById(R.id.creationSourcea)
val domainIdentityType: TextView = findViewById(R.id.domainIdentityTypea)
val domainIdentityValue: TextView = findViewById(R.id.domainIdentityValuea)
val tags: TextView = findViewById(R.id.tagsa)
val description: TextView = findViewById(R.id.descriptiona)
downloadUrl.text = image.downloadUrl
Glide.with(this).load(image.previewUrl).into(previewUrl)
id.text = image.id
filename.text = image.fileName
filesize.text = image.filesize.toString()
contentType.text = image.contentType
createdBy.text = image.createdBy
createdTimestamp.text = image.createdTimestamp
creationSource.text = image.creationSource
domainIdentityType.text = image.domainIdentityType
domainIdentityValue.text = image.domainIdentityValue
tags.text = image.tags.toString()
description.text = image.description
image.downloadUrl = downloadUrl.text as String?
image.id = id.text as String?
}
}
}
I used Paging 3 for my Recycler View that had to show some stories in the application. But while I used Paging 3, the lists did not show the item. Before I used Paging 3, the items had been showing including the details.
StoriesResponseItem.kt
#Entity(tableName = "story")
data class StoriesResponseItem(
#PrimaryKey
#field:SerializedName("id")
val id: String,
#field:SerializedName("photoUrl")
val photoUrl: String,
#field:SerializedName("createdAt")
val createdAt: String,
#field:SerializedName("name")
val name: String,
#field:SerializedName("description")
val description: String,
#field:SerializedName("lon")
val lon: Double? = null,
#field:SerializedName("lat")
val lat: Double? = null
)
ApiService.kt
interface ApiService {
#GET("stories")
suspend fun getPagingStories(
#Header("Authorization") Authorization: String,
#Query("page") page: Int,
#Query("size") size: Int
): List<StoriesResponseItem>
}
StoriesPagingSource.kt
class StoriesPagingSource(private val token: String, private val apiService: ApiService) : PagingSource<Int, StoriesResponseItem>() {
private companion object {
const val INITIAL_PAGE_INDEX = 1
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, StoriesResponseItem> {
return try {
val position = params.key ?: INITIAL_PAGE_INDEX
val responseData = apiService.getPagingStories(token, position, params.loadSize)
LoadResult.Page(
data = responseData,
prevKey = if (position == INITIAL_PAGE_INDEX) null else position - 1,
nextKey = if (responseData.isEmpty()) null else position + 1
)
} catch (retryableError: Exception) {
LoadResult.Error(retryableError)
}
}
override fun getRefreshKey(state: PagingState<Int, StoriesResponseItem>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
StoriesRepository.kt
class StoriesRepository(private val apiService: ApiService) {
fun getStories(token: String): LiveData<PagingData<StoriesResponseItem>> {
return Pager(
config = PagingConfig(
pageSize = 5
),
pagingSourceFactory = {
StoriesPagingSource(token, apiService)
}
).liveData
}
}
MainViewModel.kt
class MainViewModel(private val storiesRepository: StoriesRepository) : ViewModel() {
private val _stories = MutableLiveData<PagingData<StoriesResponseItem>>()
fun stories(token: String): LiveData<PagingData<StoriesResponseItem>> {
val response = storiesRepository.getStories(token).cachedIn(viewModelScope)
_stories.value = response.value
return response
}
}
class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return MainViewModel(Injection.provideRepository(context)) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val mainViewModel: MainViewModel by viewModels {
ViewModelFactory(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
if (applicationContext.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
val layoutManager = GridLayoutManager(this, 2)
binding.rvStories.layoutManager = layoutManager
} else {
val layoutManager = LinearLayoutManager(this)
binding.rvStories.layoutManager = layoutManager
}
val pref = AppDataStore.getInstance(dataStore)
val authViewModel = ViewModelProvider(this, ViewModelFactory(pref))[AuthViewModel::class.java]
authViewModel.loginToken().observe(this) { token: String? ->
val loginToken = "Bearer $token"
getData(loginToken)
}
}
private fun getData(loginToken: String) {
val adapter = ListStoriesAdapter()
binding.rvStories.adapter = adapter.withLoadStateFooter(
footer = LoadingStateAdapter {
adapter.retry()
}
)
lifecycleScope.launch {
mainViewModel.stories(loginToken).observe(this#MainActivity) {
adapter.submitData(lifecycle, it)
}
}
}
}
This is my repository for this project :
https://github.com/daffakurnia11/StoryApp
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 task is to open an activity with notes attached to this diary when you select a single diary.
(one-to-many)
This is how entities in the database look like:
#Entity(tableName = "word_table")
data class Word(#ColumnInfo(name = "word") val word: String,
#ColumnInfo(name = "description") val description : String
)
{
#ColumnInfo(name = "id")
#PrimaryKey(autoGenerate = true)
var id : Long = 0
}
#Entity(tableName = "note_table")
data class Note(#ColumnInfo(name = "note_name") val note : String,
#ColumnInfo(name = "text") val text : String,
#ColumnInfo(name = "diaryId") val diaryId : Long
){
#PrimaryKey(autoGenerate = true)
var idNote : Long = 0
}
Using a data class in NoteRepository.kt
data class NotesAndWords (#Embedded val word : Word,
#Relation(parentColumn = "id", entityColumn = "diaryId")
val notes : List<Note>)
And a Query in WordDao.kt
#Transaction
#Query("SELECT * from word_table ")
fun getSomeNotes() : LiveData<List<NotesAndWords>>
I get the data and save it in the NoteRepository class:
class NoteRepository (private val wordDao : WordDao) {
var allNotes : LiveData<List<NotesAndWords>> = wordDao.getSomeNotes()
suspend fun insertNote(note : Note)
{
wordDao.insertNote(note)
}
}
Then via NoteViewModel.kt passing data to NoteActivity.kt:
class NoteViewModel(application: Application) : AndroidViewModel(application) {
private val repository: NoteRepository
val allNotes: LiveData<List<NotesAndWords>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = NoteRepository(wordsDao)
allNotes = repository.allNotes
}
fun insertNote(note: Note) = viewModelScope.launch {
repository.insertNote(note)
}
}
(NoteActivity.kt)
class NoteActivity : AppCompatActivity() {
private val newWordActivityRequestCode = 1
private lateinit var noteViewModel: NoteViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_note)
val adapter = NoteListAdapter(this, intent.getLongExtra("tag", -1) ){
val intent = Intent(this, ClickedActivity::class.java)
intent.putExtra("tag", it.note)
startActivity(intent)
}
recyclerview1.adapter = adapter
recyclerview1.layoutManager = LinearLayoutManager(this)
noteViewModel = ViewModelProvider(this).get(NoteViewModel::class.java)
noteViewModel.allNotes.observe(this, Observer {
adapter.setNotes(it)
})
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK)
{
data?.getStringArrayListExtra(NewWordActivity.EXTRA_REPLY)?.let {
val note = Note(it[0], it[1], intent.getLongExtra("tag", -1))
noteViewModel.insertNote(note)
}
}
else
{
Toast.makeText(applicationContext, R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
Then, in the adapter, I use MutableMap to transform the list so that the key is the name id and the value is the notes selected on request (attached to a specific diary)
NoteListAdapter.kt:
class NoteListAdapter internal constructor(
context: Context,
val wordId: Long,
private val listener : (Note) -> Unit
) : RecyclerView.Adapter<NoteListAdapter.NoteViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
//private val mContext = context
private var notes = emptyList<NotesAndWords>() // Cached copy of words
private var notesMapped = mutableMapOf<Long, List<Note>>()
inner class NoteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val noteItemView: TextView = itemView.findViewById(R.id.textView1)
private val noteDescriptionView: TextView = itemView.findViewById(R.id.textView)
fun bindView(note: Note, listener : (Note) -> Unit) {
noteItemView.text = note.diaryId.toString()
noteDescriptionView.text = note.text
itemView.setOnClickListener {
listener(note)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_layout, parent,
false)
return NoteViewHolder(itemView)
}
override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
holder.bindView(notesMapped[wordId]!![position], listener)
}
internal fun setNotes(notes: List<NotesAndWords>) {
this.notes = notes
for (i in this.notes) {
notesMapped[i.word.id] = i.notes
}
notifyDataSetChanged()
}
override fun getItemCount() = notesMapped[wordId]!!.size
}
Database:
#Database(entities = [Word::class, Note::class], version = 2, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
private class WordDatabaseCallback(private val scope: CoroutineScope) : RoomDatabase.Callback()
{
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.wordDao())
}
}
}
suspend fun populateDatabase(wordDao: WordDao) {
//wordDao.deleteAll()
//wordDao.deleteAllNotes()
}
}
companion object {
#Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context, scope:CoroutineScope): WordRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
val instance = Room.databaseBuilder(context.applicationContext,
WordRoomDatabase::class.java, "word_database")
.addCallback(WordDatabaseCallback(scope))
//.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
I've created several diaries and one note in each of them, using the buttons to create new diaries and notes. Now, if you select several diaries in turn, then on some attempt to select a diary, a NullPointerException is issued in the adapter, in this line:
override fun getItemCount() = notesMapped[wordId]!!.size
Why is this exception thrown if notesMapped always has the wordId key?
NoteActivity is called from another activity and the diary id is passed to it
This repository on GitHub: https://github.com/Lomank123/RoomDatabase
Edit:
noteViewModel.allNotes.observe(this, Observer {
var getList = emptyList<Note>()
for(i in it)
{
if(i.word.id == wordId)
{
getList = i.notes
break
}
}
adapter.setNotes(getList)
})
I've changed the Observer in NoteActivity and changed setNotes() method in adapter, but now it returns nothing. With for() I get the right notes and give them to adapter.setNotes(). If it doesn't work, how can I get the correct list of notes?
Hi initially the map is empty and the map is returning a null value and you are checking size on a null object.
Also as a good practice do not use a map instead use a list of notes only and pass the list directly.