Problems with nested recyclerview - android

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!

Related

Is it possible to use a recyclerView inside a form?

I'm developing an app to store TV shows informations. The use can add shows and then view its collection. I want, when adding a show, to be able to also add seasons to it, and several if need be.
I have Show and Season models, and I've created an AddShowActivity with its add_show_activity layout. I've started using Android Studio not long ago so maybe this is not optimal, but I thought of using a RecyclerView inside of my layout, and then recycle an item_add_season layout in order to add as many seasons as I want while creating a show.
However, this has caused several problems to me, to which I couldn't find any answer and am currently lost as to what to do. I've put an Add Season button in my add_show_activity, which is supposed to add a new item_add_season to my RecyclerView, however I didn't know how I should go about doing that. And even if I still haven't tried it, I'm wondering how I'll be able to retrieve my data from outside of my Adapter.
So I've been wondering if it was possible to use a RecyclerView like that in order to add several seasons to my form ? And if not, how should I go about doing that ?
Below are my AddShowActivity and my AddSeasonAdapter (the recyclerview adapter).
class AddShowActivity : AppCompatActivity() {
private lateinit var editTextName: EditText
private lateinit var editTextNote: EditText
private lateinit var confirmButton: Button
private lateinit var addSeasonButton: Button
private lateinit var seasonsRecyclerView: RecyclerView
#SuppressLint("NewApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_show)
editTextName = findViewById(R.id.name_input)
editTextNote = findViewById(R.id.note_input)
seasonsRecyclerView = findViewById(R.id.seasons_recycler_view)
seasonsRecyclerView.adapter = AddSeasonAdapter(this, 0, R.layout.item_add_season)
seasonsRecyclerView.layoutManager = LinearLayoutManager(this)
confirmButton = findViewById(R.id.confirm_button)
confirmButton.setOnClickListener{
sendForm()
}
addSeasonButton = findViewById(R.id.add_season_button)
addSeasonButton.setOnClickListener {
// Add a season to the RecyclerView and update its seasonsCount
}
}
#SuppressLint("NewApi")
private fun sendForm(){
val repo = ShowRepository()
val showName = editTextName.text.toString()
val showNote = parseInt(editTextNote.text.toString())
val seasonsList = arrayListOf<SeasonModel>() // Get info from seasons adapter and create seasons list
val show = ShowModel(UUID.randomUUID().toString(), showName, showNote, seasonsList)
repo.insertShow(show)
this.finish()
}
}
class AddSeasonAdapter(val context: AddShowActivity, private var seasonsCount: Int, private val layoutId: Int) : RecyclerView.Adapter<AddSeasonAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view){
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
return ViewHolder(view)
}
#SuppressLint("NewApi")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
}
override fun getItemCount(): Int = seasonsCount
}
I've found a YouTube video explaining exactly how to do it (this one for those who wanna see it).
So basically, the solution is not to use a RecyclerView but instead a LinearLayout in which the seasons are added when clicking on the 'Add season' button. This is quite easy to do, as the only thing to do is to inflate the layout, here my item_add_season, and then add it to the LinearLayout.
So like that:
// The LinearLayout in which items are added
val seasonsList = findViewById<LinearLayout>(R.id.seasons_list)
addSeasonButton.setOnClickListener {
val seasonView: View = layoutInflater.inflate(R.layout.item_add_season, null, false)
// Initialize the seasons items components
val seasonNumber = seasonView.findViewById<EditText>(R.id.season_number_input)
val seasonNote = seasonView.findViewById<EditText>(R.id.season_note_input)
val imageClose = seasonView.findViewById<ImageView>(R.id.image_close)
imageClose.setOnClickListener {
seasonsList.removeView(seasonView)
}
// Add the add_season_layout to the linearLayout
seasonsList.addView(seasonView)
}

How to disable the auto scroll of a RecyclerView (ListAdapter) that happens when an item is updated?

BACKGROUND
I have a UI that shows a list of users' fullnames with a like/dislike button for each item. I am using a ListAdapter that under the hood uses DiffUtil and AsyncListDiffer APIs. The list of users is received as a LiveData from a Room database and it's ordered by "isLiked".
PROBLEM
Whenever the like button is tapped, Room as I am using a LiveData will re-submit the new data to the adapter. The problem is that as the list is ordered by "isLiked", the liked user will change its position and the RecyclerView will always sroll to the new position.
I don't want to see the new position of the updated item. So, how can I disable the auto scroll behavior?
WHAT I TRIED
MainActivity.kt
..
val userAdapter = UsersAdapter(this)
val ll = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recyclerView.apply {
layoutManager = ll
adapter = userAdapter
itemAnimator = null
setHasFixedSize(true)
}
viewModel.users.observe(this, {
// This will force the recycler view to scroll back to the previous position
// But it's more of a workaround than a clean solution.
val pos = ll.findFirstVisibleItemPosition()
userAdapter.submitList(it) {
recyclerView.scrollToPosition(pos)
}
})
..
UsersAdapter.kt
class UsersAdapter(
private val clickListener: UserClickListener
) : ListAdapter<UserEntity, UsersAdapter.UserViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_user, parent, false)
return UserViewHolder(view)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val userEntity = getItem(position)
holder.bind(userEntity, clickListener)
}
class UserViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val textView: TextView = view.findViewById(R.id.fullName)
private val fav: ImageButton = view.findViewById(R.id.fav)
fun bind(user: UserEntity, clickListener: UserClickListener) {
textView.text = user.fullName
val favResId = if (user.favorite) R.drawable.like else R.drawable.dislike
fav.setImageResource(favResId)
fav.setOnClickListener {
val newFav = !user.favorite
val newFavResId = if (newFav) R.drawable.like else R.drawable.dislike
fav.setImageResource(newFavResId)
clickListener.onUserClicked(user, newFav)
}
}
}
interface UserClickListener {
fun onUserClicked(user: UserEntity, isFavorite: Boolean)
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<UserEntity>() {
override fun areItemsTheSame(
oldUser: UserEntity,
newUser: UserEntity
) = oldUser.id == newUser.id
override fun areContentsTheSame(
oldUser: UserEntity,
newUser: UserEntity
) = oldUser.fullName == newUser.fullName && oldUser.favorite == newUser.favorite
}
}
}
I tried using a regular RecyclerView adapter and DiffUtil with detect moves set to false.
I added the AsyncListDiffer as well.
I tried the ListAdapter, and even tried the paging library and used the PagedListAdapter.
DiffUtil's callback changes the auto scrolling, but i couldn't get the desired behavior.
Any help is greatly appreciated!

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

Saving the instance of Recycler view during orientation change

I have a RecyclerView which was build using an Arraylist. That Arraylist consists of User defined objects named ListItem.
Each recyclerview has a card view. Each CardView holds each ListItem.
I have removed one CardView from that RecyclerView.
When I rotate the screen , A new Activity is created which results in showing the old data. But I want the recyclerview to hold only updated list and should retain the scrolled position.
ListItem class :
class ListItem(var title: String, var info: String, val imageResource: Int) {
}
MainActivity class :
class MainActivity : AppCompatActivity() {
private lateinit var mSportsData: ArrayList<ListItem>
private lateinit var mAdapter: MyAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val gridColumnCount = resources.getInteger(R.integer.grid_column_count)
recycler_view.layoutManager = GridLayoutManager(this,gridColumnCount)
mSportsData = ArrayList()
recycler_view.setHasFixedSize(true)
initializeData()
recycler_view.adapter = mAdapter
var swipeDirs = 0
if (gridColumnCount <= 1) {
swipeDirs = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
}
val helper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT or ItemTouchHelper.UP or ItemTouchHelper.DOWN,swipeDirs) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val from = viewHolder.adapterPosition
val to = target.adapterPosition
Collections.swap(mSportsData,from,to)
mAdapter.notifyItemMoved(from,to)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
mSportsData.removeAt(viewHolder.adapterPosition)
mAdapter.notifyItemRemoved(viewHolder.adapterPosition)
}
})
helper.attachToRecyclerView(recycler_view)
}
private fun initializeData() {
val sportsList : Array<String> = resources.getStringArray(R.array.sports_titles)
Log.d("Printing","$sportsList")
val sportsInfo : Array<String> = resources.getStringArray(R.array.sports_info)
val sportsImageResources : TypedArray = resources.obtainTypedArray(R.array.sports_images)
mSportsData.clear()
for (i in sportsList.indices-1) {
Log.d("Printing","${sportsList[i]},${sportsInfo[i]},${sportsImageResources.getResourceId(i,0)}")
mSportsData.add(ListItem(sportsList[i], sportsInfo[i], sportsImageResources.getResourceId(i, 0)))
}
sportsImageResources.recycle()
mAdapter = MyAdapter(mSportsData,this)
mAdapter.notifyDataSetChanged()
}
fun resetSports(view: View) {
initializeData()
}
}
MyAdapter class :
class MyAdapter(var mSportsData: ArrayList<ListItem>, var context: Context) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(context).inflate(R.layout.wordlist_item,parent,false))
}
override fun getItemCount() = mSportsData.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val listItem = mSportsData.get(position)
holder.bindTo(listItem)
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
init {
itemView.setOnClickListener(this)
}
override fun onClick(view: View) {
val currentSport = mSportsData.get(adapterPosition)
val detailIntent = Intent(context, DetailActivity::class.java)
detailIntent.putExtra("title", currentSport.title)
detailIntent.putExtra("image_resource", currentSport.imageResource)
context.startActivity(detailIntent)
}
fun bindTo(currentSport : ListItem){
itemView.heading_textview.setText(currentSport.title)
itemView.description_textview.setText(currentSport.info)
Glide.with(context).load(currentSport.imageResource).into(itemView.image_view)
}
}
}
You can restrict activity restarting in your Manifest if you have same layout for Portrait and Landscape mode.
Add this to your activity in the manifest.
<activity android:name=".activity.YourActivity"
android:label="#string/app_name"
android:configChanges="orientation|screenSize"/>
If you don't want to restrict screen orientation changes, then you can use OnSaveInstanceState method to save your older data when orientation changed. Whatever data you save via this method you will receive it in your OnCreate Method in bundle. Here is the helping link. So here as you have ArrayList of your own class type you also need to use Serializable or Parcelable to put your ArrayList in your Bundle.
Except these making ArrayList as public static is always a solution, But its not a good solution in Object Oriented paratime. It can also give you NullPointerException or loss of data, in case of low memory conditions.
It looks like initializeData is called twice since onCreate is called again on orientation change, you could use some boolean to check if data has been already initialized then skip initializing
What you are doing is you are deleting the values that are passed down to the recyclerview but when the orientation changes the recyclerview reloads from activity and the original data from activity is passed down again and nothing changes, so if you want to save the changes in recyclerview you have to change the original data in the activity so that if the view reloads the data is the same.
I think u initialize adapter in oncreate method in which the whole adapter will be recreated and all datas is also newly created when configuration changes. Because u init data in oncreate method. Try something globally maintain the list and also delete the item in the list in activity when u delete in adapter also. Or try something like view model architecture
Use MVVM pattern in the project. It will manage the orientation state.
MVVM RecyclerView example:
https://medium.com/#Varnit/android-data-binding-with-recycler-views-and-mvvm-a-clean-coding-approach-c5eaf3cf3d72

The notifyItemRangeInserted command isn't working

I'm having a problem when call the notifyItemRangeInserted of the adapter. When I call this method, nothing happens, simple as that. I've tried to set some println() in the ViewHolderAdapter, but he isn't called, so I can't view the prints.
I've tried all of the "notify" commands of the adapter, and none of these work. Simply nothing happens.
That's my MainActivity. All the objects and arrays I've tested, all of them are working like a charm. I can't understand why the notify doesn't work.
class MainActivity:AppCompatActivity(){
//Declarations of the variables
var pageNumber = 1
var limitPerPage = 5
lateinit var product: Product
var productList = ArrayList<EachProduct>()
var myAdapter =ViewHolderAdapter(productList, productList.size)
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView.layoutManager = LinearLayoutManager(this#MainActivity)
recyclerView.adapter = myAdapter
The code to add items on the list and notify the ViewHolderAdapter is
//update the product list
fun updateProductList(product:Product){
for(i in 0 until 5 step 1){
productList.add(product.produtos[i])
}
showData(productList,pageNumber*limitPerPage)//then notify
}
fun showData(productList:List<EachProduct>,productsListSize:Int){
myAdapter.notifyItemRangeInserted(0,productList.size)
}
That's my ViewHolderAdapter class
class ViewHolderAdapter(private var products: List<EachProduct>, private val productsListSize: Int): RecyclerView.Adapter<ViewHolderAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent:ViewGroup,viewType:Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_layout, parent, false)
returnViewHolder(view)
}
override fun getItemCount() = productsListSize
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.productName.text=products[position].nome
Picasso.get().load(products[position].fabricante.img).into((holder.productLogo))
}
class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
val productName:TextView=itemView.ProductName
var productLogo:ImageView=itemView.ProductLogo
}
}
I expect the ViewHolderAdapter to be called, but this is not occurring. Why is that happens? I can't understand. I'll be very grateful if someone could help me.
Because initial value of the variable productsListSize is zero. Remove it from the constructor and change adapter like this:
class ViewHolderAdapter(private var products: List<EachProduct>): RecyclerView.Adapter<ViewHolderAdapter.ViewHolder>() {
override fun getItemCount() = products.size
}
A reason can be that the initial size of the item list you want to show is 0 and the recycler view height is set to wrap content. At the moment, for this case I see 2 options:
Keep wrap content for recycler view and make sure the initial list size > 0.
Set the height of the recycler view to match_parent or a fixed size and notifyItemRangeInserted will work without issues.

Categories

Resources