We are trying to design a nested recyclerview we get the idea that TWO adapters are used. What we are not understanding is the data source construction. We are using SQLite DB for a data source. Our design is a Parent text field that describes a Department in a grocery store. Like Produce and Liquor with child Items in these Depts being tomatoes avocados and beer.
If we use two Models and two DB Tables how do we associate the child items with the Departments?
We thought about one DB Table with this format record 1 Produce tomatoes record 2 null avocado record 3 Liquor beer. This seems not so smart. So next we thought about JOINS or UNION call to make one new Table out of our two tables one with Dept and the other Items.
How would we lay out the two tables so they associate items with respective departments?
We are also guessing here that our ViewHolder needs to be a Class of its own that talks to the Parent and Child Adapters.
We will post a photo of the design we are trying to emulate (copy)
Our questions are how to design the DB Tables?
Do we need a ViewHolder Class that interfaces with the two Adapters?
How to create the two tables and what type of JOIN to call to make a new table?
We have looked at this link and the idea is great but his code does not have the same data source. One is date the other could be SQLite Kotlin Nested
OK we have a working DB and the Two Adapters DeptAdapter and ItemAdapter work BUT not at the same time. The two tables DEPT_TABLE and ITEM_TABLE have data
The view for the two tables is displayed in ListActivity with a activity_list.xml
The ListActivity CAN NOT provided both table views at one time
What we think is wrong is the recyclerview declared in recyclerview_dept.xml is not involved and all the work or views are being provided by the recyclerview in the activity_list.xml with id rvListActivity
code posted below with ONE QUESTION
class ListActivity : AppCompatActivity() {
private var RecyclerAdapter: DeptAdapter? = null
private var RecyclerAdapter2:ItemAdapter? =null
private var recyclerView: RecyclerView? = null
private var recyclerView2: RecyclerView? = null
private val db = DBHelper(this)
private var deptList:List<DEPT> = ArrayList()
private var itemList:List<ITEM> = ArrayList()
private var linearLayoutManager: LinearLayoutManager? = null
private var linearLayoutManager2: LinearLayoutManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_list)
initViews()
}// end onCreate
override fun onResume() {
super.onResume()
initDB()
}
// This is ONLY called when Activity is in onResume state
private fun initDB() {
deptList = db.queryAllDEPT()
//itemList = db.queryAllITEM()
if(deptList.isEmpty()){
title = "No Records in DB"
}else{
title = "Contact List"
}
println("########################################### onSTART")
RecyclerAdapter = DeptAdapter(deptList = deptList, context = applicationContext)
//RecyclerAdapter2 = ItemAdapter(itemList = itemList, context = applicationContext)
(recyclerView as RecyclerView).adapter = RecyclerAdapter
//(recyclerView2 as RecyclerView).adapter = RecyclerAdapter2
}
private fun initViews() {
recyclerView = this.findViewById(R.id.rvListActivity)
RecyclerAdapter = DeptAdapter(deptList = deptList, context = applicationContext)
linearLayoutManager = LinearLayoutManager(applicationContext)
(recyclerView as RecyclerView).layoutManager = linearLayoutManager!!
//recyclerView2 = this.findViewById(R.id.rvListActivity)
//RecyclerAdapter2 = ItemAdapter(itemList = itemList, context = applicationContext)
//linearLayoutManager2 = LinearLayoutManager(applicationContext)
//(recyclerView2 as RecyclerView).layoutManager = linearLayoutManager2!!
}
XML File for above Activity
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ListActivity">
<android.support.v7.widget.RecyclerView
android:id="#+id/rvListActivity"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</android.support.v7.widget.RecyclerView>
XML file with additional recyclerview
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="5dp"
android:id="#+id/list_new_card">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/tvDEPT"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginRight="16dp"
android:gravity="center_vertical"
android:text="I am tv tvDept"
android:textColor="#color/color_Black"
android:textSize="18sp"
android:textStyle="bold" />
<android.support.v7.widget.RecyclerView
android:id="#+id/rvDEPT"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</android.support.v7.widget.RecyclerView>
</LinearLayout>
</android.support.v7.widget.CardView>
Both Adapters
class DeptAdapter(deptList:List<DEPT>,internal var context: Context):RecyclerView.Adapter<DeptAdapter.DeptViewHolder>() {
private var deptList:List<DEPT> = ArrayList()
init{this.deptList = deptList}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeptViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.recyclerview_dept,parent,false)
return DeptViewHolder(view)
}
override fun getItemCount(): Int {
return deptList.size
}
override fun onBindViewHolder(holder: DeptViewHolder, position: Int) {
val items = deptList[position]
holder.item.text = items.dept
}
inner class DeptViewHolder(view: View) : RecyclerView.ViewHolder(view) {
var item: TextView = view.findViewById(R.id.tvDEPT) as TextView
}
}
Child Adapter if you will
class ItemAdapter(itemList:List<ITEM>,var context: Context):RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {
private var itemList:List<ITEM> = ArrayList()
init{this.itemList = itemList}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.recyclerview_item,parent,false)
return ItemViewHolder(view)
}
override fun getItemCount(): Int {
return itemList.size
}
override fun onBindViewHolder(holder:ItemViewHolder, position: Int) {
val items = itemList[position]
holder.item.text = items.gitem
}
inner class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
var item: TextView = view.findViewById(R.id.tvITEM) as TextView
}
}
Table Design is simple id and string both in separate Models
Our question how to show both tables in the ListActivity?
So this is really slick code. One issue I have not tied it to a SQLite DB yet.
When I get time to hook it up to a DB I will post an update.
You had a number of the ides correct two adapters is a must
And the List in a List is used here by adding the ChildModel to the ParentModel as a List<ChildModel>
This code belongs to Navendra Jha
You will need to do a little manipulation as he thought it would be fun to have the children scroll left and right independent of the parent so in the MainActivity which displays the data change this line from horizontal to vertical
layoutManager = LinearLayoutManager(this#MainActivity, LinearLayout.VERTICAL, false)
Navendra Jha used an ImageView in his code we commented that out once we understood what was happening under the hood. This is REALLY super charged full blown great code every Kotlin developer will want in her/his tool box
Related
I have a situation where I have 3 RecyclerView on a single Layout. They are dependent on each other in a certain way. Data is coming from Room Database.
Question Prototype (Problem statement): Let's say you have floors like (Floor1, Floor2, Floor3 etc.) and inside each floor you have rooms like (Room1, Room2, Room3 etc.) and inside each room you have People with name like (PersonA, PersonB, PersonC).
Given constraint is that : A person cannot be in two different rooms at same type.
Edit 1: Floors, Rooms and Persons are coming from the database in the form of a list of strings.
How would you show that using maybe ( a recycler view ) or anything on a single screen layout.
There can be infinite number of floors, rooms and persons. But that information is fetch from a room database.
My approach : (This is not a complete approach), But I am thinking like having one RecyclerView at the top which holds number of floors. We use a query to get the total floors from the database and sort them and display. During the onBindViewHolder() in the adapter for floors we have a condition which checks if there is a room associated with that floor and if there is then we make another query to fetch from database and show it (maybe using a new recycler View) but I don't know how to do that. Is nested recycler view a thing that can be used here.
It doesn't stop here as we make another query to database using that room name to find all the persons inside that room. Which is another recycler view.
I am thinking of applying this approach but I feel there are many stoppage for implementing this. And I am not sure if this is the way to handle such things.
Image for reference :
I am looking for any information if you have been through any such situations then what approach did you followed. Is there any library that can be used to simplify this task or anything that you can provide knowledge of would be helpful. Thanks!
Edit 2: What's working now :
I tried implementing the nested recycler view. Things seems to be working fine as now I have two adapters currently (for floor and rooms) and I am inflating the floor adapter from the fragment and inflating the rooms adapter from onBindViewHolder of FloorsAdapter.
The problem I am facing now that inside recycler view I am getting the list of floors correctly but for the list of rooms(in child recycler view) I am only getting it for last floor.
Check this image for reference (Floor 2 and 1 also have rooms but I am only getting room C1 which is present in last floor which is 3) :
Current code in Adapter :
class FloorsAdapter(
private val controlPanelViewModel: ControlPanelViewModel,
private val activity: FragmentActivity?
) : RecyclerView.Adapter<FloorsAdapter.FloorViewHolder>() {
private var floorList = emptyList<String>()
private lateinit var roomAdapter: RoomAdapter
inner class FloorViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): FloorViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ListItemControlPanelFloorsBinding.inflate(layoutInflater, parent, false)
return FloorViewHolder(binding.root)
}
override fun onBindViewHolder(holder: FloorsAdapter.FloorViewHolder, position: Int) {
val item = floorList[position]
ListItemControlPanelFloorsBinding.bind(holder.itemView).apply {
Timber.d("Current floor is $item, Floor List is : $floorList")
tvFloor.text = item
roomAdapter = RoomAdapter(controlPanelViewModel, activity)
rvRoomControlPanel.adapter = roomAdapter
rvRoomControlPanel.layoutManager = LinearLayoutManager(activity)
controlPanelViewModel.getAllRooms(item).observeForever(Observer {
Timber.d("List of rooms : $it")
//Finding distinct rooms
val distinct = it.toSet().toList()
Timber.d("Distinct rooms list : $distinct")
roomAdapter.roomList(distinct)
})
}
}
#SuppressLint("NotifyDataSetChanged")
fun floorList(floors: List<String>) {
this.floorList = floors
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return floorList.size
}
}
class RoomAdapter(
private val controlPanelViewModel: ControlPanelViewModel,
private val activity: FragmentActivity?
) : RecyclerView.Adapter<RoomAdapter.RoomViewHolder>() {
private var roomList = emptyList<String>()
inner class RoomViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ListItemControlPanelRoomsBinding.inflate(layoutInflater, parent, false)
return RoomViewHolder(binding.root)
}
override fun onBindViewHolder(holder: RoomViewHolder, position: Int) {
val item = roomList[position]
ListItemControlPanelRoomsBinding.bind(holder.itemView).apply {
Timber.d("Current room is $item")
tvRoom.text = item
}
}
#SuppressLint("NotifyDataSetChanged")
fun roomList(room: List<String>) {
this.roomList = room
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return roomList.size
}
}
Code in Fragment for inflating the Floors Adapter:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.rvFloors.adapter = adapter
binding.rvFloors.layoutManager = LinearLayoutManager(requireContext())
controlPanelViewModel.getAllFloors.observe(viewLifecycleOwner, Observer{
Timber.d("List is $it")
//Remove duplicates from received list
val distinct = it.toSet().toList()
Timber.d("List after removing duplicates: $distinct")
adapter.floorList(distinct)
})
Timber.d("Adapter: $adapter" )
}
Edit 3: Going through the logs I found something which explains why is it happening. The list of floors is getting executed first because of this
controlPanelViewModel.getAllFloors.observe(viewLifecycleOwner, Observer{
Timber.d("List is $it")
//Remove duplicates from received list
val distinct = it.toSet().toList()
Timber.d("List after removing duplicates: $distinct")
adapter.floorList(distinct)
})
As we are inflating the rooms recycler view from floors adapter here
roomAdapter = RoomAdapter(controlPanelViewModel, activity)
rvRoomControlPanel.adapter = roomAdapter
rvRoomControlPanel.layoutManager = LinearLayoutManager(activity)
controlPanelViewModel.getAllRooms(item).observeForever(Observer {
Timber.d("List of rooms : $it")
//Finding distinct rooms
val distinct = it.toSet().toList()
Timber.d("Distinct rooms list : $distinct")
roomAdapter.roomList(distinct)
})
It is going to be little delayed as data is being fetched from Room. So instead of doing something like (Floor 1 -> Room1, Room2 we are getting something like (Floor1, Floor2 -> Room3, Room4). The data about rooms for previous floors is getting lost.
What to do here? How to stop the execution of next floors unless we have fetched all the rooms and shown using the textview.
I have tried replicate the exact scenario with one vertical recycler view and horizontal recycler view. Here is the thing, one vertical recyclerview for Floors. Then one in floor item, list of room item views can be added to vertical linear layout container. A single room item view contains horizontal recyclerview for person.
Floor Vertical RV -> Adding Custom Room Items to Vertical LL -> Person
Horizontal RV
Hope it helps.
Activity:
class MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView = findViewById(R.id.mainRv)
recyclerView.apply {
adapter = FloorAdapter(getFloors(), this#MainActivity)
layoutManager = LinearLayoutManager(this#MainActivity)
setHasFixedSize(true)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
}
fun getFloors(): List<Floor> {
val list = ArrayList<Floor>()
for (i in 0 until 6) {
list.add(Floor())
}
return list
}
}
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/mainRv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Adapters:
class FloorAdapter(val floors: List<Floor>, val context:Context) : RecyclerView.Adapter<FloorViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FloorViewHolder {
return FloorViewHolder(LayoutInflater.from(context).inflate(R.layout.floor_item,null))
}
override fun onBindViewHolder(holder: FloorViewHolder, position: Int) {
holder.bindView(floors[position])
}
override fun getItemCount(): Int {
return floors.size
}
}
class PersonAdapter(val persons: List<Person>, val context:Context) : RecyclerView.Adapter<PersonViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder {
return PersonViewHolder(LayoutInflater.from(context).inflate(R.layout.person_item,null))
}
override fun onBindViewHolder(holder: PersonViewHolder, position: Int) {
holder.bindView(persons[position])
}
override fun getItemCount(): Int {
return persons.size
}
}
View Holders:
class PersonViewHolder(val view:View) : RecyclerView.ViewHolder(view) {
fun bindView(person: Person) {
view.findViewById<TextView>(R.id.person_tv).text = "Person:${person.personId}"
}
}
class FloorViewHolder(val view:View) : RecyclerView.ViewHolder(view){
fun bindView(floor: Floor){
val str = "Floor : ${floor.floorId}"
view.findViewById<TextView>(R.id.floor).text = str
view.findViewById<LinearLayout>(R.id.room_container).let { ll->
ll.removeAllViews()
floor.rooms.forEach{
val myView= LayoutInflater.from(view.context).inflate(R.layout.room_item,null)
myView.findViewById<TextView>(R.id.room).text = "RoomId :${it.roomId}"
myView.findViewById<RecyclerView>(R.id.persons_rv)?.apply {
adapter = PersonAdapter(it.persons,view.context)
layoutManager = LinearLayoutManager(view.context,LinearLayoutManager.HORIZONTAL,false)
setHasFixedSize(true)
}
ll.addView(myView)
}
}
}
}
Data Class:
data class Floor(val floorId: Int = (0..900).random()) {
var rooms = arrayListOf<Room>()
init {
for (i in 0 until 50) {
rooms.add(Room())
}
}
}
data class Room(val roomId: Int = (0..900).random()) {
var persons = arrayListOf<Person>()
init {
for (i in 0 until 200) {
persons.add(Person())
}
}
}
data class Person(val personId: Int = (0..900).random())
floor_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/floor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="22sp"
android:textStyle="bold"
android:textColor="#android:color/holo_red_dark"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="#+id/room_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/floor"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
person_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:padding="18dp"
android:id="#+id/person_tv"
android:textSize="18sp"
android:layout_margin="10dp"
android:background="#android:color/holo_blue_light"
android:textColor="#color/black"
android:layout_height="wrap_content">
</com.google.android.material.textview.MaterialTextView>
room_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:padding="20dp"
android:layout_height="match_parent">
<TextView
android:id="#+id/room"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#android:color/holo_green_dark"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/persons_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:layout_constraintTop_toBottomOf="#id/room" />
</androidx.constraintlayout.widget.ConstraintLayout>
I have two buttons on my MainActivity. Both buttons open the same new activity but show a different listView depending on which one is clicked. I have been playing about with it and up to now I can get it to show on the MainActivity but can not figure out how get it so that it opens up on the second activity instead. I figure I need to somehow use the adapter in the new activity and then somehow pass the ListArray to it?? I have not been able to get it to work.
the code I have at the moment is that works is
MainActivity.kt
class MainActivity : AppCompatActivity() {
lateinit var button: Button
lateinit var button2: Button
lateinit var test_list: ListView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button = findViewById(R.id.button)
button2 = findViewById(R.id.button2)
button.setOnClickListener {
val intent = Intent(this#MainActivity, ItemsList::class.java)
test_list = findViewById(R.id.test_list)
var breeds = arrayOf("")
var breeds2 = arrayListOf<Breeds>()
breeds2.add(Breeds("type1"))
breeds2.add(Breeds("type2"))
breeds2.add(Breeds("type3"))
breeds2.add(Breeds("type4"))
breeds2.add(Breeds("type5"))
var adapter: MyAdapter
adapter = MyAdapter(this, breeds2)
test_list.adapter = adapter
test_list.setOnItemClickListener { parent, view, position, id ->
Log.d("Test list", breeds[position])
startActivity(intent)
}
}
button2.setOnClickListener {
val intent = Intent(this#MainActivity, ItemsList::class.java)
test_list = findViewById(R.id.test_list)
var breeds = arrayListOf("")
var breeds2 = arrayListOf<Breeds>()
breeds2.add(Breeds("breed1"))
breeds2.add(Breeds("breed2"))
breeds2.add(Breeds("breed3"))
breeds2.add(Breeds("breed4"))
breeds2.add(Breeds("breed5"))
var adapter: MyAdapter
adapter = MyAdapter(this,breeds2)
test_list.adapter = adapter
test_list.setOnItemClickListener { parent, view, position, id ->
Log.d("Test list", breeds[position])
startActivity(intent)
}
}
}
}
MyAdapter.kt
import android...
class MyAdapter(private val context: Context, private val dataSource :ArrayList<Breeds>) : BaseAdapter(){
private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)as LayoutInflater
override fun getCount(): Int {
return dataSource.size
}
override fun getItem(position: Int): Any {
return dataSource[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
var rowView:View = inflater.inflate(R.layout.activity_items_list, parent, false)
val textView : TextView = rowView.findViewById(R.id.list_item_name)
textView.text = dataSource.get(position).breed
return rowView
}
}
breeds.kt
class Breeds {
var breed : String =""
constructor(breed: String) {
this.breed = breed
}
}
//I want to have the list view open up in this activity
ItemsList.kt
import...
class ItemsList : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_items_list)
}
}
//my xml files are
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#id/button"
/>
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:id="#+id/test_list"/>
</androidx.constraintlayout.widget.ConstraintLayout>
activity_items_list.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/list_item_name"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="10dp"
android:textColor="#color/black"
/>
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:id="#+id/test_list"/>
</androidx.constraintlayout.widget.ConstraintLayout>
You could technically find a way to pass a list between two Activitys through an Intent, but passing objects starts to get a little complicated because there are a limited set of types you can put into a Bundle, and limitations on how much data you can pass
In this case, since you just have a list of Strings, you could use the putExtra method that takes a String array, and pull it out in the other activity:
// in first activity
intent.putExtra("list key", breeds2.toTypedArray())
// in second activity - could be missing (null result) so I'm using an empty default
val list = intent.getStringArrayExtra("list key")?.toList() ?: emptyList()
// now use it in your list adapter
But generally that's not a good idea. Ideally, your data (i.e. the lists) would be stored where both Activities can access it, and all you pass between Activities is some kind of ID, so Activity 2 can fetch the correct list. An easy way for this example is to just stick them in top-level vals that both Activities can see:
// this needs to be outside of both Activities - stick it in another file if you like
val list1 = listOf(Breed("breed1"), Breed("breed2")) // etc
val list2 = listOf(Breed("type1"), Breed("type2")) // etc
// button 1 listener:
intent.putExtra("list type", 1)
// button 2 listener:
intent.putExtra("list type", 2)
// in activity 2:
val listType = intent.getIntExtra("list type") // missing default is -1
val listToUse = if (listType == 1) list1 else list2 // simple either/or fallback
// use the list in your list adapter
but if your data can change (like modified lists), you'll need to be persisting that data, e.g. in SharedPreferences, a DataStore, or a database. You'd probably have another class that provides data, where your activities can request a particular list.
This isn't exactly how I'd organise the lists or fetch them, but hopefully that gives you the basic idea - store the data elsewhere in the app, and let activities pass simple identifiers so they can fetch a particular piece of data, rather than trying to pass entire data structures between activities
I have recyerlView. I loaded data from the server. When it initially loads it height is normal, but when I go to the next activity through item click. And come back it slowly increasing the height of the child. I tried to debug this and found that onResume api call causing the issue. But What I am doing wrong in layout I don't get it.
FirstLayout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/reyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
FirstActivity.kt
class FirstActivity : BaseActivity() {
lateinit var binding: FirstLayoutActivityLayoutBinding
private val viewModel: FirstViewModel by inject()
private var listAdapter: ListAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupViewModel()
binding = FirstLayoutActivityLayoutBinding.inflate(layoutInflater)
setContentView(binding.root)
}
private fun setupViewModel() {
viewModel.livedata.observe(this, { list ->
setupAdapter(list)
})
}
private fun setupAdapter(list: List<String>) {
initializeAdapter(list)
listAdapter?.updateItemsList(list)
binding.recyclerView.apply {
addItemDecoration(HeaderItemDecoration(context))
val itemDecorator = DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
itemDecorator.setDrawable(ContextCompat.getDrawable(context, R.drawable.divider)!!)
addItemDecoration(itemDecorator)
adapter = listAdapter
}
}
private fun initializeAdapter(list: List<String>) {
listAdapter = ListAdapter(list.toMutableList())
}
override fun onResume() {
super.onResume()
viewModel.fetchItem() // noraml retrofit call
}
}
HeaderItemDecoration is used from this answer.
ListAdapter.kt
class ListAdapter(private val list: MutableList<String>) : RecyclerView.Adapter<Adapter.MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
ListLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bindItem(list[position])
}
override fun getItemCount(): Int {
return list.size
}
inner class MyViewHolder(private val binding: ListLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
fun bindItem(s: String) {
binding.cool.text = s
}
}
}
ListLayout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/root"
android:background="#color/red"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/cool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
I am adding my view to my Youtube Link. Please look at what the issue is. Thanks
I didn't add any logic to increase the height
looks like a problem with multiple ItemDecorations, which are set every time new data is fetched. to be exact: multiple DividerItemDecorations, which adds small padding to the items (for dividing them). note methods name starts with add..., so every time you are adding new, old stays there and every instance of this divider is adding some padding. if you would implement e.g. pull-to-refresh or auto-refresh in background every e.g. 10 secs then every refresh GUI method call (setupAdapter) would add some space without leaving Activity. currently you are fetching data only once, in onResume, so every move-activity-to-foreground action will add one divider (when data will be fetched properly)
move this part of code to onCreate for setting dividers only once, thats proper place for some additional styling by code
binding.recyclerView.apply {
addItemDecoration(HeaderItemDecoration(context))
val itemDecorator = DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
itemDecorator.setDrawable(ContextCompat.getDrawable(context, R.drawable.divider)!!)
addItemDecoration(itemDecorator)
}
and inside your setupAdapter method set only adapter to RecyclerView, don't style it (multiple times)
private fun setupAdapter(list: List<String>) {
initializeAdapter(list)
listAdapter?.updateItemsList(list)
binding.recyclerView.adapter = listAdapter
}
Hey I have Reyclerview with DiffUtill using ListAdapter. I added element through submitList function. But when updating the list view is not redrawing the element. Until I used notifyDataSetChanged() or setting adapter again. So what the use case of DiffUtill?. What is the proper way of doing to redraw item when item is updated in list as well as in reyclerview.
MainActivity
class MainActivity : AppCompatActivity() {
private var list = mutableListOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
private lateinit var binding: ActivityMainBinding
var i = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
Log.e("List", " $list")
val intAdapter = IntAdapter()
binding.recylerview.adapter = intAdapter
intAdapter.submitList(list)
binding.button.setOnClickListener {
list.add(++i)
intAdapter.submitList(list)
// binding.recylerview.adapter = intAdapter
// intAdapter.notifyDataSetChanged()
}
}
}
IntAdapter
class IntAdapter : ListAdapter<Int, IntViewHolder>(comparator) {
companion object {
private val comparator = object : DiffUtil.ItemCallback<Int>() {
override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IntViewHolder {
return IntViewHolder(
IntLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: IntViewHolder, position: Int) {
holder.bindItem(getItem(position))
}
}
IntViewHolder
class IntViewHolder(val binding: IntLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
fun bindItem(item: Int?) {
binding.intNumber.text = item.toString()
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/recylerview"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
int_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/intNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
You need to ensure you pass a different instance of List each time you call submitList. If you pass a List, mutate it, and then pass that same List instance again, DiffUtil is only comparing the same list to itself, so it will assume nothing has changed in the list and won't update anything. It doesn't have some sort of memory of what the List contained back when you first submitted it.
To generalize further, you must not use a mutable List with ListAdapter at all. ListAdapter assumes the list you pass to submitList does not change. If you mutate it, there can be unexpected bugs.
Two ways to resolve this in your code.
Create a read-only copy of the list each time you pass it:
intAdapter.submitList(list.toList())
Don't use a MutableList at all. Create a new List every time you modify what should be in the List. This is the simpler, less error-prone solution.
class MainActivity : AppCompatActivity() {
private var list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
private lateinit var binding: ActivityMainBinding
var i = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
Log.e("List", " $list")
val intAdapter = IntAdapter()
binding.recylerview.adapter = intAdapter
intAdapter.submitList(list)
binding.button.setOnClickListener {
list += ++i // create a new list from old list contents plus a new item
intAdapter.submitList(list)
}
}
}
Side note: when you have var combined with Mutable____ that should be kind of a red flag. It should be rare that you need two different ways to change something. That is error-prone.
You need to submit a new List, currently you are submitting that same List which has been mutated.
Inside your click listener try something like :
binding.button.setOnClickListener {
val current = intAdapter.currentList
val update = current + (current.size + 1) // returns a new list with added value
intAdapter.submitList(update)
}
Example with this logic :
I have an RecyclerView which holds some CardViews and each CardView contains EditText which multiplies the user given amount times a specific rate (the rate comes from an endpoint and the rate is different per row). For the CardViews I am using data binding.
Use case of the app:
The app should show how much for example 2, 7.89, 14.34, or 110 € are in other currencies.
User enters an amount (in the EditText) in any line, each line has a "rate" (rate comes from an API endpoint) field with a different value
The user entered amount gets multiplied by the "rate"
Each row in the RecyclerView should be updated
Now is the question how to update the text of all EditTexts elements in a RecyclerView with two-Way data binding
This is my data class for data binding:
data class CurrencyItem(
var flag: String,
var shortName: String,
var fullName: String,
var rate: Double
) : BaseObservable() {
#Bindable
var rateTimesAmount: String = (CurrencyApplication.userEnteredAmount * rate).toString()
set(amount) {
val amountAsDouble = amount.toDouble()
val number2digits: Double = String.format("%.2f", amountAsDouble).toDouble()
CurrencyApplication.userEnteredAmount = number2digits
field = number2digits.toString()
notifyPropertyChanged(BR.rateTimesAmount)
}
}
This is my EditText in the item_currency.xml
<EditText
android:id="#+id/currency_rate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:lines="1"
android:maxLength="8"
android:text="#={currency.rateTimesAmount}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="1183.068" />
This is my Application class which stores the user entered amount:
class CurrencyApplication : Application() {
companion object {
var userEnteredAmount: Double = 1.00
}
}
Here I access the RecyclerView through Kotlin Android Extensions:
recycler_view.apply {
setHasFixedSize(true)
itemAnimator = DefaultItemAnimator()
adapter = currencyAdapter
}
Here is the RecyclerView from activity_main.xml
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
android:clipToPadding="false"
android:paddingBottom="8dp"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
Here is the Adapter for the RecyclerView:
class CurrencyAdapter(
val currencies: ArrayList<CurrencyItem>
) : RecyclerView.Adapter<CurrencyViewHolder>() {
override fun getItemCount() = currencies.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CurrencyViewHolder {
val itemCurrencyBinding: ItemCurrencyBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.item_currency,
parent,
false
)
return CurrencyViewHolder(itemCurrencyBinding)
}
override fun onBindViewHolder(holder: CurrencyViewHolder, position: Int) {
holder.itemCurrencyBinding.currency = currencies[position]
}
fun setUpCurrencies(newCurrencies: List<CurrencyItem>) {
currencies.clear()
currencies.addAll(newCurrencies)
notifyDataSetChanged()
}
}
class CurrencyViewHolder(val itemCurrencyBinding: ItemCurrencyBinding) :
RecyclerView.ViewHolder(itemCurrencyBinding.root)
You might probably want to create another Observable in -say- your viewModel or adapter and bind it as a variable to the adapter items.
Like so:
class CurrencyAdapter: RecyclerView.Adapter<...>() {
val rate = ObservableField(0.0)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ViewDataBinding>(inflater, viewType, parent, false)
binding.setVariable(BR.rate, rate)
return YourViewHolder(binding.root)
}
}
If you encounter issues with other views eagerly updating the rate variable, try to making a custom data binding adapter to only allow views triggering the updates when they are in focus.
#BindingAdapter("android:textAttrChanged")
fun TextView.setOnTextAttrChangedInFocus(listener: InverseBindingListener) {
addTextChangedListener(afterTextChanged = {
if(isFocused) {
listener.onChange()
}
})
}
(example uses androidx.core extension)
I hope it helps ;)
Check out teanity it might help you figure out some stuff like this faster.