Android Paging 3 with Room - android

I'm migrating from Paging 2 to Paging 3. The app stores a large dataset in a database using Room, and I can load the data from Room and display it okay. The issue I have is as soon as the app makes a change to the database, it crashes.
Code Snippets
IssueRepository
#Query("SELECT * FROM isssue WHERE project_id = ?")
fun findAllInProject(projectId:Int): PagingSource<Int, IssueListBean>
In the function onCreateView
val dataSource = DB.store.issueRepository().findAllInProject(project.id)
val pageConfig = PagingConfig(50)
val pager = Pager(pageConfig, null) { dataSource }.flow
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
pager.collectLatest { data ->
adapter.submitData(data)
}
}
class PagingAdapter : PagingDataAdapter<IssueListBean, PagingAdapter.ViewHolder>(EntityComparator()) {
inner class ViewHolder(private val adapterBinding: ItemIssueListBinding) : RecyclerView.ViewHolder(adapterBinding.root) {
fun bind(position: Int) {
val issueListBean = getItem(position)
adapterBinding.label.text = issueListBean.label
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemIssueListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(position)
}
}
So when users tap on an item they can edit it. As soon the item is saved via Room the app crashes with the following exception:
java.lang.IllegalStateException: An instance of PagingSource was re-used when Pager expected to create a new
instance. Ensure that the pagingSourceFactory passed to Pager always returns a
new instance of PagingSource.
Am I using Paging 3 wrong? I can't find many articles online talk about using Room as data source where you make changes.

The lambda you pass to Pager() should return a new instance of the data source each time, so move the call to findAllInProject() into that lambda, like
val pager = Pager(pageConfig, null) {
DB.store.issueRepository().findAllInProject(project.id)
}.flow

Related

Paging Library 3 Loading states not working

I have an application which fetches movies from the moviedb api , i'm using paging library 3 to page the data , i have set up everything and data is showing properly , the only thing that is not working in loading states , upon reading little bit more about loading state adapter , i got to know that it only works when fetching data from db after using remote mediator , i might be wrong , someone please corrects me , i would appreciate any help ..
Code
val layoutManager = LinearLayoutManager(this)
binding.recyclerView.layoutManager = layoutManager
binding.recyclerView.setHasFixedSize(true)
movieAdapter = TrendingMovieAdapter(object : MovieListener{
override fun onMovieSelected(movieId: Int) {
Intent(this#MainActivity,DetailsActivity::class.java).apply {
putExtra("id",movieId)
startActivity(this)
}
}
})
binding.recyclerView.adapter = movieAdapter.withLoadStateHeaderAndFooter(
footer = LoaderAdapter(),
header = LoaderAdapter()
)
lifecycleScope.launch {
movieViewModel.getPagedTrendingMovies().collectLatest {
movieAdapter.submitData(it)
}
}
LoadStatesAdapter class
class LoaderAdapter : LoadStateAdapter<LoaderAdapter.ViewHolder>() {
inner class ViewHolder(var binding : LoaderItemBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
return ViewHolder(
DataBindingUtil.inflate(LayoutInflater.from(parent.context),
R.layout.loader_item,parent,false)
)
}
override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
holder.binding.progressBar.isVisible = loadState is LoadState.Loading
}
}
I also config footer load state adapter same your code, there's doesn't anything to be wrong. Hmm, you can try call executePendingBindings() after call holder.binding.progressBar.isVisible = loadState is LoadState.Loading.
This is onBindViewHolder() func:
override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) { holder.binding.progressBar.isVisible = loadState is LoadState.Loading executePendingBindings() }

Update Current Page or Update Data in Paging 3 library Android Kotlin

I am new in Paging 3 library in android kotlin. I want unlimited data. So I found Paging 3 library is helpful in my case. I used PagingSource to create a list. I am not using Room. I have nested recyclerView. I am using PagingDataAdapter with diff util for my Recyclerview. I used the recommended tutorial for Paging library from codelab and I succeeded without any problem. I am facing difficult to update the item. I used paging source to create list and inside list i have some data which are coming from sever. I completely all this without any problem. But how to update adapter or notify data has changed in reyclerview. I already mechanism to fetch updated list. I searched how to update the adapter in some place but every where is mention to use invalidate() from DataSource. DataSource is used in paging 2 right?. Now this is inside the Paging 3 as per Documentation in Migrate to Paging 3. I used Flow to retrieve data. This code is inside viewmodel class.
fun createRepo(data: List<String>, repository: RepositoryData): Flow<PagingData<UnlimitData>> {
return repository.getStreamData(data).cachedIn(viewModelScope)
}
I am passing list, which is coming from sever. getStreamData function return the items with int and string data. My Data class
data class UnlimitData(val id: Int, val name: String)
createRepo is calling in my activity class to send data in adpater.
lifecycleScope.launch {
viewModel.createRepo(serverData,repository).collectLatest { data ->
adapter.submitData(data)
}
}
This is my Adapter code:-
class unlimitedAdapter() :
PagingDataAdapter<UnlimitData, RecyclerView.ViewHolder>(COMPARATOR) {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
(holder as UnlimitedViewHolder).bind(item)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return UnlimitedViewHolder.create(parent)
}
companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<UnlimitData>() {
override fun areItemsTheSame(oldItem: UnlimitData, newItem: UnlimitData): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: UnlimitData, newItem: UnlimitData): Boolean = oldItem == newItem
}
}
}
I added logic to insert/Update data in list using RetroFit. My list is updated successfully, but i am unable to refresh reyclerview.
Thanks in advance.
In order for paging to pick up new items, you will need to call PagingSource.invalidate() to inform Pager that it needs to fetch a new PagingSource and reload pages. You'll want to keep track of all the PagingSources your factory produces and invalidate them anytime you update the backing dataset from network.
EDIT: Something like this, but this is a very rough prototype
abstract class InvalidatingPagingSourceFactory<K,V> : () -> PagingSource<K,V> {
private val list = mutableListOf()
abstract fun create()
override final fun invoke() {
create().also { list.add(it) }
}
fun invalidate() {
while (list.isNotEmpty()) { list.removeFirst().invalidate() }
}
}
When We use adapter.refresh() method it will refresh the latest data and bind with the layout.
But It is not reLoading the recycler view with Position.
In that case, you will get the wrong position.
Because in DiffUtils you match correctly. So, whenever you refresh, it will bind only that new and not bonded data.
So, The solution is you have to make false the DiffUtils. In that case, PageSource will update the full list and the position will be updated.
Like : oldItem.id ==-1
private val COMPARATOR = object : DiffUtil.ItemCallback<UnlimitData>() {
override fun areItemsTheSame(oldItem: UnlimitData, newItem: UnlimitData): Boolean =
oldItem.id ==-1
override fun areContentsTheSame(oldItem: UnlimitData, newItem: UnlimitData): Boolean = oldItem == newItem
}

Access DB from adapter (Kotlin)

I have a list of categories and i want to show the amount of items in each category. Using Room with MVVM architecture basically i want to use simple query in my adapter, to return its value (amount of items)
DAO
#Query("SELECT COUNT(id) FROM items WHERE listId=:listID")
suspend fun countItems(listID: Long):Int
Repo
suspend fun countItems(id: Long): Int{
return itemsDao.countItems(id)
}
Adapter
class ListsAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<ListsAdapter.ListViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var lists = mutableListOf<ListItem>()
inner class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val listName: TextView = itemView.findViewById(R.id.single_list_name)
val listIcon: ImageView = itemView.findViewById(R.id.single_list_icon)
val wAmount: TextView = itemView.findViewById(R.id.single_list_amount)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListsAdapter.ListViewHolder {
val itemView = inflater.inflate(R.layout.single_list, parent, false)
return ListViewHolder(itemView)
}
override fun getItemCount() = lists.size
override fun onBindViewHolder(holder: ListsAdapter.ListViewHolder, position: Int) {
val current = lists[position]
holder.listName.text = current.name
// holder.wAmount.text =
holder.itemView.setOnClickListener {
val bundle = bundleOf("list_id" to current.id,"list_name" to current.name)
holder.itemView.findNavController().navigate(R.id.action_listsFragment_to_nav_items_list, bundle)
}
}
internal fun setLists(lists: List<ListItem>) {
this.lists = lists.toMutableList()
notifyDataSetChanged()
}
internal fun listToDelete(viewHolder: RecyclerView.ViewHolder) : ListItem{
val position = viewHolder.adapterPosition
return lists[position]
}
internal fun removeList(viewHolder: RecyclerView.ViewHolder){
lists.removeAt(viewHolder.adapterPosition)
notifyItemRemoved(viewHolder.adapterPosition)
}
}
Should it be done over ViewModel, but in that case it has to be passed to adapter? Or maybe there is better (cleaner) way to do it? Any help is appreciated. Thanks
Adapters shouldn't be responsible for loading data from storage. The clean way to do it is to access the repo in your ViewModel, then pass the data to your Adapter. This also gives you the benefit of being able to handle errors in a straightforward way, as it would be easy to update the layout containing Adapter easily, unlike the spaghetti you'll need to do this from the adapter, in the addition to being a wrong design of course.
Another step would be to introduce a more layered architecture,like Uncle Bob's clean architecture

Can i use ViewModelProvider and Observe in RecyclerView.Adapter?

I user MVVM and RecyclerView in this app so the recycle view show the list perfectly but when i add the view model to adapter i get an error in the logcat
Your activity is not yet attached to the Application instance. You can't request ViewModel before onCreate call.
i am new in this MVVM and i know is this possible or is any other way to do this
this is my adapter class with the viewHolder
class KeefAdapter : RecyclerView.Adapter<KeefViewHolder>() {
var dataOfAllKeef = listOf<String>()
init {
dataOfAllKeef = arrayListOf("Marijuwana" , "Bango" , "Weed" , "Hash")
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): KeefViewHolder {
lateinit var binding: KeefSingleItemBinding
binding = DataBindingUtil.inflate(LayoutInflater.from(parent.context) , R.layout.keef_single_item , parent , false)
val viewModel:OrderYourKeefViewModel = ViewModelProvider(OrderYourKeef()).get(OrderYourKeefViewModel::class.java)
binding.orderViewModelWithSingle = viewModel
viewModel.count.observe(OrderYourKeef(), Observer { newCountOfHash->
binding.root.theCountOfHash.text = newCountOfHash.toString()
})
return KeefViewHolder(binding.root)
}
override fun getItemCount() = dataOfAllKeef.size
override fun onBindViewHolder(holder: KeefViewHolder, position: Int) {
val item = dataOfAllKeef[position]
holder.keefName.text = item
if (item.equals("Marijuwana")) {
holder.keefImage.setImageResource(R.mipmap.marijuana)
} else if (item.equals("Bango")) {
holder.keefImage.setImageResource(R.mipmap.bango)
} else if (item.equals("Weed")) {
holder.keefImage.setImageResource(R.mipmap.weed)
} else if (item.equals("Hash")) {
holder.keefImage.setImageResource(R.mipmap.hashesh)
}
}
}
class KeefViewHolder(itemView:View) : RecyclerView.ViewHolder(itemView) {
var keefName:TextView = itemView.keefName
var keefImage: ImageView = itemView.keefImage
var increase: Button = itemView.increaseTheCount
var decrease: Button = itemView.minusTheCount
var theCountOfKeef: TextView = itemView.theCountOfHash
}
I think this is not the correct way to implement the MVVM pattern.
You have to call the viewModel = ViewModelProviders in your Activity. And after fetching the list items, pass it to your adapter and call the notifyDataSetChanged():
updateListItems(newListItems: List<YourItem>) {
currentItems = newListItems
notifyDataSetChanged()
}
Read more about it here
Adapter seems to be designed to be used rather passively than actively.
In OP's code, he would observe and get newCountOfHash in onCreateViewHolder to set it to binding.root.theCountOfHash.text. So this is a case that Adapter would actively seek and grab a value.
To avoid this 'active' Adapter, we should define Adapter behaving passively. Locally define countOfHash as Adapter's field value. The Adapter shouldn't mind countOfHash is LiveData or not. It just looks the field value.
class KeefAdapter : RecyclerView.Adapter<KeefViewHolder>() {
var countOfHash
override fun onBindViewHolder(holder: KeefViewHolder, position: Int) {
// You should not do this in onCreateViewHolder
// because that is done only once on creation time.
// (not invoked later again)
binding.root.theCountOfHash.text = countOfHash
}
}
Then outside of the Adapter, from Activity or Fragment that holds the Adapter, you may update Adapter.countOfHash with an Observer:
val viewModel:OrderYourKeefViewModel
= ViewModelProvider(OrderYourKeef()).get(OrderYourKeefViewModel::class.java)
viewModel.count.observe(OrderYourKeef(), Observer { newCountOfHash ->
Adapter.countOfHash = newCountOfHash.toString()
})
(Note: I'm not using Kotlin actively, there may be some syntax mistakes)

Problems with nested recyclerview

I have a nested recyclerview which should look like in the .
I implemented it according to this helpful site.
The problem is, that I sometimes have a user with hundreds of items and in that case, it takes half a minute to open the activity.
I have a room database in the backend with two linked tables with foreign keys (users and items) and I select all users to get a user/item list where the items are a list in the user-table.
class userWithItems: (id: Int, name: String, ... ,List)
and I create the inner recycler view with the List of items in the adapter.
Would it be better to make one List UserItems (userid:Int, username:String, ... itemid:Int, itemList) and group them for the outer rv.
Or is there a possibility to get rid of the nested rv and make the design with just one recyclerview-list?
Or is there another solution to make the nested recyclerview work even if there are many items for a user?
code for the adapters:
// Code in Activity: (oncreate)
val recyclerView = findViewById<RecyclerView>(R.id.rv_users)
val adapter = UserAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
mainViewModel = ViewModelProviders.of(this, PassIntViewModelFactory(this.application, online_id)).get(MainViewModel::class.java!!)
mainViewModel.userList.observe(this, Observer {
it?.let {
adapter.setUserList(it)
}
})
data class UsersWithItems(
val id:Int, val username: String, val address, // fields from user table
val items: List<Items> // list of items for current user
)
data class Items (
val id: Int, val itemtext: String, val itemlocation: String, val image: String // ...
)
// UserAdapter (outside)
class UserAdapter internal constructor(
context: Context
) : RecyclerView.Adapter() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var userList = emptyList<UsersWithItems>()
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val username: TextView = itemView.findViewById(R.id.user_name)
val num_pos: TextView = itemView.findViewById(R.id.user_num_pos)
val address: TextView = itemView.findViewById(R.id.user_addr)
val rv:RecyclerView = itemView.findViewById(R.id.rv_user_items)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val myItemView = inflater.inflate(R.layout.rv_row_user, parent, false)
return MyViewHolder(myItemView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val current = userList[position]
holder.username.text="${current.user?.username}"
holder.num_pos.text="${current.items?.size}"
holder.address.text = "${current.user?.address}"
val adapter = UserItemAdapter(holder.rv.context)
adapter.setItems(current.items!!)
holder.rv.adapter = adapter
holder.rv.layoutManager = LinearLayoutManager(holder.rv.context,LinearLayout.VERTICAL,false)
}
internal fun setUserList(userList: List<UsersWithItems>){
this.userList=userList
notifyDataSetChanged()
}
override fun getItemCount() = userList.size
}
class UserItemAdapter internal constructor(
context: Context
) : RecyclerView.Adapter() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var itemList = emptyList<Items>()
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val itemtext: TextView = itemView.findViewById(R.id.item_text)
val itemlocation:TextView = itemView.findViewById(R.id.item_location)
val image: ImageView = itemView.findViewById(R.id.item_image)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val myItemView = inflater.inflate(R.layout.rv_row_user_items, parent, false)
return MyViewHolder(myItemView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val current = itemList[position]
holder.itemtext.text="${current.itemtext}"
holder.itemlocation.text = current.itemlocation
if (current.image.length>0) {
val image = Base64.decode(current.image, 0)
val bitmap = BitmapFactory.decodeByteArray(image, 0, image.size)
holder.image.setImageBitmap(bitmap)
}
}
internal fun setItems(items: List<Items>){
this.itemList=items
notifyDataSetChanged()
}
override fun getItemCount() = itemList.size
}
E. Reuter i have been through this situation the thing is the approach is quite correct by using nested Recycler View. Your code seems to be good. but the queries which you are using to query database. I think you should use queries in Background or on the other threas and show result as you get them instead of querying it from OnCreate or from main thread. Because getting this many items in one go can possibly create lag to activity and decreasing performance. try this out if you have not yet and let me know. What happens. Thanks...
I am editing my answer. the other thing you could do is if you have more than certain amount of items then instead of getting them at the first you should use some thing like pagination to load certain amount of items at once to avoid this lag.
Here i am attaching the code to query certain amount of data per load....
SApp.database!!.resultDao().loadAllUsersByPage(5, 10)
#Query("SELECT * FROM Result LIMIT :limit OFFSET :offset")
fun loadAllUsersByPage(limit: Int, offset: Int): List<Result>
Thank you very much for your answer. I think that paging is really a good approach. But I cannot add the pageing directly since I am getting my data from a roomdatabase like this:
#Query(SELECT * FROM users)
fun getData(): LiveData<List<userWithItems>>
and the actual items are added by room because of a relation between user and items I will have to change this behavior.
I will try something like
#Query(SELECT * FROM users)
fun getUserData(): LiveData<List<Users>>
and then try to add an LiveData observer in the outer recyclerview to get the items in a separate query which uses paging.
I solved the problem. When I thought about pagination it came into my mind that the problem could be that the inner recyclerview has a height of wrap_content and so it needs to build all of the items and makes the rv useless. When I make the height of the inner rv 250dp, it works quite even with 2000 items.
So now I just have to figure out a way to always find the optimal height for the inner rv and solve the scrolling problem but the original problem is solved.
Special thanks to Aman B!

Categories

Resources