Overview
Have you ever thought about getting rid of recyclerView adapters & managing them? Especially in the case of nested RecyclerViews.
Problems with RecyclerView’s Adapters
- Things can get messy when we have many views to maintain where everything just gets one big pile of code which results in no code readability at all and the situation gets more tricky with updates.
- But that is just tip of the iceberg because when you have n number of nested RecyclerViews & Adapters to maintain that can result in just big mess
- when you have to make some updates, you would be afraid to touch the code because what you see in there is a pile of code.
Data binding & Binding Adapter joins the rescue party.
If you are new to data binding please refer to this documentation of android
- How about one Adapter with all your RecyclerView needs which uses a binding adapter & data binding which sets up everything for you, increases your productivity and doesn’t create a mess.
- This can be very helpful when working in teams so everyone can understand, change & update everything without fear of breaking some piece of code.
Code Time
class GlobalAdapter<T>( private val layoutId: Int, var mutableList: MutableList<T>, private val br: Int, private var clickListener: RvClickListener, private val brs: Map<Int, Any> ) : RecyclerView.Adapter<GlobalAdapter<T>.ViewHolder>()
Here as we can see I am taking a number of parameters which are needed to set up everything for the adapter. Let’s understand each of them.
- Layout Id : represents your recyclerView’s row file.
- Mutable List : represents your data to be added into adapter
- Br: represents your model class from the list
- Click listener : reference to clicklistener which will give you click callbacks in your activity/fragment.
- mapOfBr<Int,Any> : this map will have a list of br variables which will be initialized in your row file from the adapter.
private lateinit var binding: ViewDataBinding override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { binding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), layoutId, parent, false) return ViewHolder(binding) }
In the onCreateViewHolder i am initializing binding from your layout file.
override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.binding.setVariable(br, mutableList[holder.adapterPosition]) brs.forEach { binding.setVariable(it.key, it.value) } holder.binding.setVariable( BR.click, View.OnClickListener { v -> clickListener.click( v, mutableList[holder.adapterPosition], holder.adapterPosition, this as GlobalAdapter<Any> ) }) holder.binding.executePendingBindings() }
In the onBindViewHolder
- Your model is initialized into your layout’s model variable
- Map of brs is then initialized
- Then your clicklistener is initialized to send out all clickevents & as the reference of itemclick listener is passed on to nested RecyclerViews you will be able to get all call backs in your activity/fragment.
override fun getItemCount() = mutableList.size
getItemCount() will set the count from your list.
inner class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
In viewHolder class I am passing binding which is initially initialized in onCreatViewHolder
@JvmStatic @BindingAdapter("layout", "list", "click") fun <T> setRecyclerView( view: RecyclerView, layout: Int, list: List<T>?, click: RvClickListener ) { if (list != null) { val adapter = GlobalAdapter( layout, list.toMutableList(), BR.model, clickListener = click, mapOf(BR.itemclick to click) ) view.adapter = adapter } }
Understanding the code of the binding adapter.
Binding adapter needs three parameters but our GlobalAdapter needs four.
- Layout Id
- List
- Click listener
- For the mapOfBr I am initializing it manually from the code, initially you will pass your itemClickListener reference to your layout file from activity/fragment & incase if you have nested RecyclerViews i will set it into br.itemclick to pass on the initial reference into row file of your nestedRecyclerView.
So you will be able to get all callbacks of clicklisteners into one listener itself.
Let’s understand how this RvClickListener interface works.
interface RvClickListener { fun click( view: View, item: Any?, position: Int, adapter: GlobalAdapter<Any> ) }
- You implement this interface in your activity/fragment
- You then initialized your itemclicklistener variable into your activity/fragment
- You pass your itemclicklistener to bindingAdapter
- BindingAdapter passes it to adapter
- Adapter initializes it to binding file
- You get all your callbacks in your implemented method
The Big Picture
How everything works as a whole?
Model class
data class Articles( val title: String, val urlToImage: String, val description: String, var isChecked: Boolean = false ) data class News(val category: List<NewsCategory>) data class NewsCategory(var name: String, val list: List<Articles>)
Here we have three classes News, NewsCategory & Articles classes
News & NewsCategory contains a list which is displayed in recyclerView.
Activity
class MainActivity : AppCompatActivity(), View.OnClickListener { private lateinit var binding: ActivityMainBinding private lateinit var mainClickListener: RvClickListener override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setListeners() binding.itemclick = mainClickListener binding.click = this@MainActivity binding.model = giveModel() } override fun onClick(v: View?) { Toast.makeText(this, "Clicked", Toast.LENGTH_SHORT).show() } private fun setListeners() { mainClickListener = object : RvClickListener { override fun click( view: View, item: Any?, position: Int, adapter: GlobalAdapter<Any>, ) { when (item) { is Articles -> { when (view.id) { R.id.rv_nested_news_main_cv -> { view as MaterialCardView adapter as GlobalAdapter<Articles> adapter.mutableList.map { it.isChecked = false } item.isChecked = true adapter.notifyItemRangeChanged(0, adapter.itemCount) } } } is NewsCategory -> { when (view.id) { R.id.rv_news_main_cv -> { view as MaterialCardView view.background.setTint(Color.CYAN) Toast.makeText(this@MainActivity, "$position", Toast.LENGTH_SHORT) .show() } } } } } } } }
Here View.OnClickListener, RvClickListener & model which is then initialized with variables in your row/item layout file.
Here you’ll get a cleaner way to handle clicks because each view will be different & each view will have different model classes based on the layout so you’ll be able to handle clicks easily.
Note: Updating data also needs data to be updated from your model too so if your data source is api or database you need to call it again when you delete or update the data, otherwise data still remains in your model & thus even after deleting it from adapter it will be added again by binding adapter.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <layout> <data> <import type="android.view.View" /> <variable name="model" type="com.recyclerviewDemo.model.News" /> <variable name="click" type="View.OnClickListener" /> <variable name="itemclick" type="com.recyclerviewDemo.RvClickListener" /> </data> <..> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_news" click="@{itemclick}" layout="@{@layout/row_news_item_rv}" list="@{model.category}" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:orientation="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_toBottomOf="@id/rv_title" tools:context=".MainActivity" tools:listitem="@layout/row_news_nested_item_rv"/> <../>
Here in the layout file we have 3 variables as we had in MainActivity. As we pass this data in our binding adapter with the layout file it will then initialize GlobarAdapter.
Now let’s see how the nested RecyclerView is going to be initialized.
row_news_item_rv.xml
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View" /> <variable name="model" type="com.recyclerviewDemo.model.NewsCategory" /> <variable name="itemclick" type="com.recyclerviewDemo.RvClickListener" /> <variable name="click" type="android.view.View.OnClickListener" /> </data> <..> <androidx.recyclerview.widget.RecyclerView click="@{itemclick}" layout="@{@layout/row_news_nested_item_rv}" list="@{model.list}" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="5dp" android:orientation="horizontal" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/row_news_nested_item_rv" /> <../>
Here we have the same three variables which we had before, if we recall the code from GlobalAdapter’s onCreateViewHolder, we were initializing the same three variables from there according to positions thus we’ll have our variables initialized here.
Now we can further initialize another recyclerView in this row file the same way we did for the first one.
<?xml version="1.0" encoding="utf-8"?> <layout> <data> <import type="android.view.View" /> <variable name="model" type="com.recyclerviewDemo.model.Articles" /> <variable name="click" type="View.OnClickListener" /> <variable name="itemclick" type="com.recyclerviewDemo.RvClickListener" /> </data> <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/rv_nested_news_main_cv" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="5dp" android:clickable="true" android:focusable="true" android:onClick="@{click::onClick}" app:cardBackgroundColor="@{model.isChecked?@color/teal_200:@color/white}" app:cardElevation="10dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.google.android.material.imageview.ShapeableImageView android:id="@+id/iv_nested_news_image" urlToImage="@{model.urlToImage}" android:layout_width="match_parent" android:layout_height="200dp" android:padding="5dp" android:scaleType="fitXY" app:shapeAppearanceOverlay="@style/CircleImage" /> <TextView android:id="@+id/tv_nested_news_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="@{click::onClick}" android:padding="5dp" android:text="@{model.title}" android:textAppearance="?attr/textAppearanceHeadline6" /> <TextView android:id="@+id/tv_nested_news_description" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="5dp" android:text="@{model.description}" android:textAppearance="?attr/textAppearanceBody2" android:textColor="?android:attr/textColorSecondary" /> </LinearLayout> </com.google.android.material.card.MaterialCardView> </layout>
Now in the nested row file I am simply displaying the data.
You can have multiple recyclerViews here and there & it totally considers
each of them independently so you don’t really have something like a parent child concept applied here.
Here is the final output
All the source code is available at this Github repo, pull requests are always welcomed.
Read: Features of C# 9.0
Conclusion
This approach can indeed help many android developers out there & sometimes things can get tricky in the Android world, so things might come handy.
Thanks for your time, see you around folks.