Level: Beginner
If you need to show a dynamic list of items in your android app, recycler view is your best choice, recycler view is designed to display a large list of data while not consuming so much memory. RecyclerView achieves this by recycling views that are not visible on the screen and uses them to represent some other item that should be displayed.
What you need to know or at least be familiar with:
- ConstraintLayout and the layout editor
- View Binding
- Kotlin’s MutableList and data class
- AlertDialog (not required)
What we’ll create
Creating the layout
In this example im going to use a single recyclerview that takes the width and height of the entire screen, i gave it an id of “rv_people”
Note: this example uses LinearLayoutManager as the layout manager for our recycler view you can replace it with GridLayoutManager or StaggredGridLayoutManager but don’t forget to specify the “spanCount” attribute if you want to use thoes
<?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:id="@+id/rv_people"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="1dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="1dp"
android:layout_marginBottom="1dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<Button
android:id="@+id/btn_add_rmv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="22dp"
android:layout_marginBottom="24dp"
android:text="+ / -"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
To display our data in the list we need to specify how a single item will look like in the list, in our example we want to display some names with icons next to them.
we’re going to need to create a layout resource file for that, you can create a new Resource layout file from the project panel in android studio right click the layout directory > New > Layout Resource File.
choose a name for your new layout file, usually recycler view items like these end with “item” so since our resource represents a person i named it “person_item” the resource file looks like this:
<?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="100dp">
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@android:drawable/btn_star_big_on" />
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintBottom_toBottomOf="@+id/iv_avatar"
app:layout_constraintEnd_toStartOf="@+id/iv_avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/iv_avatar"
tools:text="Ali" />
</androidx.constraintlayout.widget.ConstraintLayout>
Data Class
data class Person(
val name: String,
//this is not really an image url (link) its just going to hold a drawable resource id as the annotation suggests
@DrawableRes val imageUrl: Int
)
Adapter Class
The adapter is the most important part of a recycler view, its where each item of the list gets populated and inflated to the UI.
First lets create a class and call it PersonAdapter, this class will inherit from ListAdapter this ListAdapter takes a generic type which means we now have to tell it what kind of data it should work with and in our case the data type is Person !. we included that in the angle brackets and also specified that we are going to use RecyclerView.ViewHolder as our view holder
class PersonAdapter : ListAdapter<Person,RecyclerView.ViewHolder>(DIFF_CALLBACK){
}
Now whats DIFF_CALLBACK?, its a reference to a utility class that can calculate the difference between 2 lists and converts the old list to the new list without having to replace every item, in other words it just changes what needs to be changed instead of changing everything. it does that using the logic that we provide by overriding areItemsTheSame and areContentsTheSame
class PersonAdapter : ListAdapter<Person,RecyclerView.ViewHolder>(DIFF_CALLBACK){
companion object{
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Person>() {
override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.name == newItem.name || oldItem.imageUrl == newItem.imageUrl
}
}
}
}
Now lets implement our view holder as an inner class inside PersonAdapter, the bind function in this class is what is going to be triggered when the item is displayed its job is to assign the correct name and image to our list item.
class PersonAdapter : ListAdapter<Person,RecyclerView.ViewHolder>(DIFF_CALLBACK){
companion object{
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Person>() {
override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.name == newItem.name || oldItem.imageUrl == newItem.imageUrl
}
}
}
inner class ViewHolder(val itemBinding: PersonItemBinding):
RecyclerView.ViewHolder(itemBinding.root) {
fun bind(item: Person){
itemBinding.tvName.text = item.name
itemBinding.tvName.setOnClickListener {
Toast.makeText(it.context,"name: ${item.name}",Toast.LENGTH_LONG).show()
}
itemBinding.ivAvatar.setImageResource(item.imageUrl)
}
}
}
Lets implement the rest of the abstract functions required from ListAdapter
class PersonAdapter : ListAdapter<Person,RecyclerView.ViewHolder>(DIFF_CALLBACK){
companion object{
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Person>() {
override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.name == newItem.name || oldItem.imageUrl == newItem.imageUrl
}
}
}
private lateinit var binding: PersonItemBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
RecyclerView.ViewHolder {
binding = PersonItemBinding.inflate(LayoutInflater.from(parent.context),parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder){
val item = getItem(position)
holder.bind(item)
}
}
inner class ViewHolder(val itemBinding: PersonItemBinding):
RecyclerView.ViewHolder(itemBinding.root) {
fun bind(item: Person){
itemBinding.tvName.text = item.name
itemBinding.tvName.setOnClickListener {
Toast.makeText(it.context,"name: ${item.name}",Toast.LENGTH_LONG).show()
}
itemBinding.ivAvatar.setImageResource(item.imageUrl)
}
}
}
Linking the adapter with RecyclerView and displaying data
In order for the recycler view to work in needs to be liked to an adapter to link our recycler view with the adapter all we need is to create an instance of our adapter class and assign it using the “adapter” property from the recycler view, then we just call the submitList function on the adapter and pass our list of Person and the list should be displayed 🙂
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val namesList = mutableListOf(
Person("Ali", R.drawable.baseline_catching_pokemon_24),
Person("Ali", R.drawable.baseline_child_care_24),
Person("Ayoub", R.drawable.baseline_catching_pokemon_24),
Person("Mohab", R.drawable.baseline_catching_pokemon_24),
Person("Ali", R.drawable.baseline_catching_pokemon_24),
Person("Hatem", R.drawable.baseline_child_care_24),
Person("Ali", R.drawable.baseline_catching_pokemon_24),
Person("Ali", R.drawable.baseline_cruelty_free_24),
Person("Ali", R.drawable.baseline_catching_pokemon_24),
Person("Derar", R.drawable.baseline_delete_24),
Person("Ali", R.drawable.baseline_catching_pokemon_24),
Person("Mo", R.drawable.ic_launcher_background),
Person("Ali", R.drawable.baseline_cruelty_free_24),
Person("Hatem", R.drawable.baseline_cruelty_free_24),
Person("Ali", R.drawable.baseline_cruelty_free_24),
Person("Ali", R.drawable.baseline_cruelty_free_24),
Person("Mo", R.drawable.ic_launcher_background),
Person("Mo", R.drawable.ic_launcher_background),
Person("Mo", R.drawable.ic_launcher_background),
Person("Ali", R.drawable.baseline_cruelty_free_24),
Person("Ali", R.drawable.baseline_catching_pokemon_24),
)
//create the adapter
private val adapter = PersonAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//assign the adapter to the recyclerView
binding.rvPeople.adapter = adapter
//submit the list
adapter.submitList(namesList)
//extra: using an alert dialog to add and remove items form the list
binding.apply {
btnAddRmv.setOnClickListener {
showDialog()
}
}
}
private fun showDialog() {
val dialogEtBinding = DialogEtBinding.inflate(layoutInflater)
AlertDialog.Builder(this).setView(dialogEtBinding.root).setPositiveButton("+") { _, _ ->
try {
dialogEtBinding.apply {
addToIndex(etIndex.text.toString().toInt())
etIndex.setText("")
}
} catch (e: Exception) {
Toast.makeText(this, "Error: ${e.localizedMessage}", Toast.LENGTH_LONG).show()
}
}.setNegativeButton(" - ") { _, _ ->
try {
dialogEtBinding.apply {
removeItemFromIndex(etIndex.text.toString().toInt())
etIndex.setText("")
}
} catch (e: Exception) {
Toast.makeText(this, "Error: ${e.localizedMessage}", Toast.LENGTH_LONG).show()
}
}.show()
}
private fun removeItemFromIndex(index: Int) {
namesList.removeAt(index - 1)
adapter.notifyItemRemoved(index - 1)
}
private fun addToIndex(index: Int) {
val strings =
listOf("Derar", "SomeName", "Ali", "Ayoub", "Majid", "Yousra", "Islam", "Mohab")
val drawableResList = listOf(
R.drawable.baseline_catching_pokemon_24,
R.drawable.baseline_delete_24,
R.drawable.baseline_cruelty_free_24,
R.drawable.baseline_child_care_24,
R.drawable.baseline_directions_run_24,
R.drawable.ic_launcher_foreground
)
//add to list
namesList.add(index - 1, Person(strings.random(), drawableResList.random()))
//notify the adapter
adapter.notifyItemInserted(index - 1)
}
}
If we launch our app now we should be able to display our list as well as remove or add items.
The sample project is available on Github.