Divide and Conquer with ConcatAdapter

Lucas Nobile
6 min readAug 4, 2020

TL;DR: With ConcatAdapter we can breakdown a screen into small pieces, so, we can manage each piece as a feature and adapt to changes. It’s like playing with Lego blocks!

What is ConcatAdapter?

ConcatAdapter allows us to display the contents of multiple adapters, in a sequence.

Benefits of using ConcatAdapter:

  1. Keep our layouts simple, this is with fewer lines of XML, therefore, readable and easier to maintain.
  2. Each adapter has a single responsibility and can be re-usable.
  3. Adapt to changes: adding new features and applying changes to the screen will be easier.

Real-Life Use Cases

Let’s see the following Product Detail screen:

The layout would be something like this (some attributes are omitted for simplicity):

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout>
<ScrollView> <androidx.constraintlayout.widget.ConstraintLayout> <ImageView
android:id="@+id/iv_product"
... />
<TextView
android:id="@+id/tv_manufacturer"
tools:text="Beanfields"
... />
<TextView
android:id="@+id/tv_product_name"
tools:text="Jalapeño Nacho Bean Chips"
.../>
<TextView
android:id="@+id/tv_short_description"
tools:text="5.5 oz bag"
... />
<TextView
android:id="@+id/tv_rating"
tools:text="4.62"
... />
<TextView
android:id="@+id/tv_price"
tools:text="$2.99"
... />
<TextView
android:id="@+id/tv_discount"
tools:text="$3.79"
... />
<TextView
android:id="@+id/tv_description_title"
tools:text="Description"
... />
<TextView
android:id="@+id/tv_description"
tools:text="A description"
... />
<TextView
android:id="@+id/tv_ingredients_title"
tools:text="Ingredients"
... />
<TextView
android:id="@+id/tv_ingredients"
tools:text="Ingredients list"
... />
</androidx.constraintlayout.widget.ConstraintLayout>

</ScrollView>
<Button
android:id="@+id/btn_add_to_cart"
tools:text="Add to Cart"
... />
</androidx.constraintlayout.widget.ConstraintLayout>

Now, we want to add the following features:

  1. Product image gallery.
  2. Product out of stock section.

Adding the new features to the existing layout is possible (for sure) but we will end up having a layout that is difficult to follow up and to maintain.

I know what you’re thinking: we can create separate layouts and use the include XML tag. That’s true, but the final layout will be large as well. There is a better option 😉

ConcatAdapter to the rescue!

Imagine the ConcatAdapter is a list of adapters. Each adapter is a section of the screen. So, each adapter is like a Lego block. Then, we will be building our screen using Lego blocks, one by one. It’s like playing with Legos 🙌

For our ConcatAdapter to exists we will need to:

  1. Add
implementation “androidx.recyclerview:recyclerview:1.2.0-alpha04
// or greater

2. Use a RecyclerView as the main component of our layout.

Adding a RecyclerView gives us a lot of flexibility:

  1. For screens that are not scrollable beforehand, we can make them scrollable by adding more features, that for sure will come, don’t you think? 🤔
  2. For screens that are scrollable, we will have it by default.
  3. Customize our RecyclerView.

Now, the layout for Product Detail screen will be:

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_product_detail"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_add_to_cart"
tools:text="Add to Cart"
.../>
</androidx.constraintlayout.widget.ConstraintLayout>

How simple, readable, easy to maintain do you think the above layout is? 🤓

The Lego blocks

Let’s breakdown our view into Lego blocks!

  1. Image gallery adapter:

2. Price adapter:

3. Description adapter:

4. Ingredients adapter:

Now, let’s create an Adapter:

class ProductPriceAdapter(
val product: Product,
val listener: (Product) -> Unit
) : RecyclerView.Adapter<ProductPriceAdapter.ViewHolder>() {
override fun getItemCount() = 1 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ViewHolder(parent.inflate(R.layout.view_item))
override fun onBindViewHolder(holder: ViewHolder, position: Int) =
holder.bind(product, listener)
inner class ViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
fun bind(product: Product, listener: (Product) -> Unit) =
with(itemView) {
itemTitle.text = product.title
itemImage.loadUrl(product.url)
setOnClickListener { listener(product) }
}
}
}

The main differences with the Adapters we’re used to create are:

  1. We are not passing a list of “something” to our adapter but a single Model to grab data from it and bind that data to UI fields (TextViews, ImageViews, and so)
  2. getItemCount() = 1 because each adapter represents one section of the screen, one Lego block, remember? 🤓

Aaand, the layout for Product Price Adapter will be:

<androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/tv_manufacturer"
tools:text="Beanfields"
... />
<TextView
android:id="@+id/tv_product_name"
tools:text="Jalapeño Nacho Bean Chips"
.../>
<TextView
android:id="@+id/tv_short_description"
tools:text="5.5 oz bag"
... />
<TextView
android:id="@+id/tv_rating"
tools:text="4.62"
... />
<TextView
android:id="@+id/tv_price"
tools:text="$2.99"
... />
<TextView
android:id="@+id/tv_discount"
tools:text="$3.79"
... />
</androidx.constraintlayout.widget.ConstraintLayout>

Ok, I’m sold, let’s create a ConcatAdapter and start adding adapters to it, please! 🧐

In our Product Detail view we will have:

private fun setupAdapters() {
binding.rvProductDetail.layoutManager =
LinearLayoutManager(activity)
val concatAdapter = ConcatAdapter()

val productGalleryAdapter = ProductGalleryAdapter(/*data for
adapter*/)
concatAdapter.addAdapter(productGalleryAdapter)
val productPriceAdapter = ProductPriceAdapter(/*data for
adapter*/)
concatAdapter.addAdapter(productPriceAdapter)
val productOutOfStockAdapter = ProductOutOfStockAdapter(/*data
for adapter*/)
if (product.isOutOfStock) {
concatAdapter.addAdapter(productOutOfStockAdapter)
}
val productDescriptionAdapter = ProductDescriptionAdapter(/*data
for adapter*/)
concatAdapter.addAdapter(productDescriptionAdapter)
val productIngredientsAdapter = ProductIngredientsAdapter(/*data
for adapter*/)
concatAdapter.addAdapter(productIngredientsAdapter)
binding.rvProductDetail.adapter = concatAdapter
}

And that’s it! 🔥

ConcatAdapter is also useful in a Home Screen, where we need to show a lot of data, from different sources, different sections and some cool stuff:

ConcatAdapter and DataBinding

If you are like me and you’re using DataBinding, we can create a BaseAdapter that will receive a ViewModel (to execute the bindings) and a layout resource ID:

open class BaseViewModelBindingAdapter(
private val viewModel: BaseViewModel,
private val layoutResId: Int
) : RecyclerView.Adapter<BaseViewModelBindingAdapter.ViewHolder>() {
lateinit var binding: ViewDataBinding override fun getItemCount() = 1 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
binding = DataBindingUtil.inflate(
layoutInflater, layoutResId, parent, false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(viewModel)
}
inner class ViewHolder(private val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(viewModel: BaseViewModel) {
binding.setVariable(BR.viewModel, viewModel)
binding.executePendingBindings()

}
}
}

And your Lego block will be just 😉

class ProductPriceAdapter(viewModel: ProductPriceViewModel) :
BaseViewModelBindingAdapter(viewModel,
R.layout.product_price_item)

Let me share with you all the repo (the branch is merge-adapter):

And my tech talk about ConcatAdapter for GDG Córdoba, Argentina (in Spanish):

Conclusion

ConcatAdapter allows us to breakdown a screen in small pieces and to create each piece using a RecyclerView’s adapter. We will have layouts that will be simpler, readable and easier to maintain.

Also, each adapter has a single responsibility and can be re-usable.

And last but not least, we can adapt to changes, this is, adding new features and applying changes to the screen will be easier.

From now on, next time you open an app, start thinking about how you can break it down into Lego blocks 😉

You can find more info about ViewHolders, using stable ids, data changes notifications and finding ViewHolder position using ConcatAdapter in Florina Muntenescu article.

Thanks for reading and start using ConcatAdapter today 🙌

--

--