Ilya Nekrasov, Mahtalitet , KODE android developer
For two and a half years in the android development, I managed to work on completely different projects: from the social network for motorists and the Latvian bank to the federal bonus system and the third airline carrier. Anyway, in each of these projects I came across tasks that required finding non-classical solutions when implementing lists using the RecyclerView class.
This article - the fruit of preparing for the performance at DevFest Kaliningrad'18, as well as communication with colleagues - will be especially useful for novice developers and those who have used only one of the existing libraries.
To begin with, let's dig a little into the essence of the issue and the source of pain, namely, the expansion of the functional in developing the application and the complication of the used lists.
Let's imagine a situation when a customer who wants a mobile application for a shop selling rubber ducks addresses the studio.
The project is developing rapidly, new ideas arise regularly and are not decorated in a long-term roadmap.
First, the customer asks us to show a list of existing products and with a click to issue an application for delivery. You don’t need to go far for a decision: we use the classic set from RecyclerView , a simple self-written adapter for it and an Activity .
For the adapter, we use homogeneous data, one ViewHolder and simple logic of binding.
class DucksClassicAdapter( private val data: List<Duck>, private val onDuckClickAction: (Duck) -> Unit ) : RecyclerView.Adapter<DucksClassicAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val rubberDuckView = LayoutInflater.from(parent.context).inflate(R.layout.item_rubber_duck, parent, false) return ViewHolder(rubberDuckView) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val duck = data[position] holder.divider.isVisible = position != 0 holder.rubberDuckImage.apply { Picasso.get() .load(duck.icon) .config(Bitmap.Config.ARGB_4444) .fit() .centerCrop() .noFade() .placeholder(R.drawable.duck_stub) .into(this) } holder.clicksHolder.setOnClickListener { onDuckClickAction.invoke(duck) } } override fun getItemCount() = data.count() class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val rubberDuckImage: ImageView = view.findViewById(R.id.rubberDuckImage) val clicksHolder: View = view.findViewById(R.id.clicksHolder) val divider: View = view.findViewById(R.id.divider) } }
Over time, the customer has an idea to add another category of goods to the rubber ducks, which means that they will have to add a new data model and a new layout. But the most important thing is that the adapter will have another ViewType , with which you can determine which ViewHolder to use for a specific list item.
After that, headings are added to categories, in which each category can be collapsed and expanded to simplify the orientation of users in the store. This is plus another ViewType and ViewHolder for headers. In addition, it is necessary to complicate the adapter, since it is necessary to keep a list of open groups and use it to check the need to hide and display an item by clicking on the title.
class DucksClassicAdapter( private val onDuckClickAction: (Pair<Duck, Int>) -> Unit, private val onSlipperClickAction: (Duck) -> Unit, private val onAdvertClickAction: (Advert) -> Unit ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { var data: List<Duck> = emptyList() set(value) { field = value internalData = data.groupBy { it.javaClass.kotlin } .flatMap { groupedDucks -> val titleRes = when (groupedDucks.key) { DuckSlipper::class -> R.string.slippers RubberDuck::class -> R.string.rubber_ducks else -> R.string.mixed_ducks } groupedDucks.value.let { listOf(FakeDuck(titleRes, it)).plus(it) } } .toMutableList() duckCountsAdapters = internalData.map { duck -> val rubberDuck = (duck as? RubberDuck) DucksCountAdapter( data = (1..(rubberDuck?.count ?: 1)).map { count -> duck to count }, onCountClickAction = { onDuckClickAction.invoke(it) } ) } } private val advert = DuckMockData.adverts.orEmpty().shuffled().first() private var internalData: MutableList<Duck> = mutableListOf() private var duckCountsAdapters: List<DucksCountAdapter> = emptyList() private var collapsedHeaders: MutableSet<Duck> = hashSetOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { VIEW_TYPE_RUBBER_DUCK -> { val view = parent.context.inflate(R.layout.item_rubber_duck, parent) DuckViewHolder(view) } VIEW_TYPE_SLIPPER_DUCK -> { val view = parent.context.inflate(R.layout.item_duck_slipper, parent) SlipperViewHolder(view) } VIEW_TYPE_HEADER -> { val view = parent.context.inflate(R.layout.item_header, parent) HeaderViewHolder(view) } VIEW_TYPE_ADVERT -> { val view = parent.context.inflate(R.layout.item_advert, parent) AdvertViewHolder(view) } else -> throw UnsupportedOperationException("view type $viewType without ViewHolder") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is HeaderViewHolder -> bindHeaderViewHolder(holder, position) is DuckViewHolder -> bindDuckViewHolder(holder, position) is SlipperViewHolder -> bindSlipperViewHolder(holder, position) is AdvertViewHolder -> bindAdvertViewHolder(holder) } } private fun bindAdvertViewHolder(holder: AdvertViewHolder) { holder.advertImage.showIcon(advert.icon) holder.advertTagline.text = advert.tagline holder.itemView.setOnClickListener { onAdvertClickAction.invoke(advert) } } private fun bindHeaderViewHolder(holder: HeaderViewHolder, position: Int) { val item = getItem(position) as FakeDuck holder.clicksHolder.setOnClickListener { changeCollapseState(item, position) } val arrowRes = if (collapsedHeaders.contains(item)) R.drawable.ic_keyboard_arrow_up_black_24dp else R.drawable.ic_keyboard_arrow_down_black_24dp holder.arrow.setImageResource(arrowRes) holder.title.setText(item.titleRes) } private fun changeCollapseState(item: FakeDuck, position: Int) { val isCollapsed = collapsedHeaders.contains(item) if (isCollapsed) { collapsedHeaders.remove(item) } else { collapsedHeaders.add(item) } // 1 to add items after header val startPosition = position + 1 if (isCollapsed) { internalData.addAll(startPosition - ADVERTS_COUNT, item.items) notifyItemRangeInserted(startPosition, item.items.count()) } else { internalData.removeAll(item.items) notifyItemRangeRemoved(startPosition, item.items.count()) } notifyItemChanged(position) } @SuppressLint("SetTextI18n") private fun bindSlipperViewHolder(holder: SlipperViewHolder, position: Int) { val slipper = getItem(position) as DuckSlipper holder.duckSlipperImage.showIcon(slipper.icon) holder.duckSlipperSize.text = ": ${slipper.size}" holder.clicksHolder.setOnClickListener { onSlipperClickAction.invoke(slipper) } } private fun bindDuckViewHolder(holder: DuckViewHolder, position: Int) { val duck = getItem(position) as RubberDuck holder.rubberDuckImage.showIcon(duck.icon) holder.rubberDuckCounts.adapter = duckCountsAdapters[position - ADVERTS_COUNT] val context = holder.itemView.context holder.rubberDuckCounts.layoutManager = LinearLayoutManager(context, HORIZONTAL, false) } override fun getItemViewType(position: Int): Int { if (position == 0) return VIEW_TYPE_ADVERT return when (getItem(position)) { is FakeDuck -> VIEW_TYPE_HEADER is RubberDuck -> VIEW_TYPE_RUBBER_DUCK is DuckSlipper -> VIEW_TYPE_SLIPPER_DUCK else -> throw UnsupportedOperationException("unknown type for $position position") } } private fun getItem(position: Int) = internalData[position - ADVERTS_COUNT] override fun getItemCount() = internalData.count() + ADVERTS_COUNT class DuckViewHolder(view: View) : RecyclerView.ViewHolder(view) { val rubberDuckImage: ImageView = view.findViewById(R.id.rubberDuckImage) val rubberDuckCounts: RecyclerView = view.findViewById(R.id.rubberDuckCounts) } class SlipperViewHolder(view: View) : RecyclerView.ViewHolder(view) { val duckSlipperImage: ImageView = view.findViewById(R.id.duckSlipperImage) val duckSlipperSize: TextView = view.findViewById(R.id.duckSlipperSize) val clicksHolder: View = view.findViewById(R.id.clicksHolder) } class HeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) { val title: TextView = view.findViewById(R.id.headerTitle) val arrow: ImageView = view.findViewById(R.id.headerArrow) val clicksHolder: View = view.findViewById(R.id.clicksHolder) } class AdvertViewHolder(view: View) : RecyclerView.ViewHolder(view) { val advertTagline: TextView = view.findViewById(R.id.advertTagline) val advertImage: ImageView = view.findViewById(R.id.advertImage) } } private class FakeDuck( val titleRes: Int, val items: List<Duck> ) : Duck private fun ImageView.showIcon(icon: String, placeHolderRes: Int = R.drawable.duck_stub) { Picasso.get() .load(icon) .config(Bitmap.Config.ARGB_4444) .fit() .centerCrop() .noFade() .placeholder(placeHolderRes) .into(this) } private class DucksCountAdapter( private val data: List<Pair<Duck, Int>>, private val onCountClickAction: (Pair<Duck, Int>) -> Unit ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val view = parent.context.inflate(R.layout.item_duck_count, parent) return CountViewHolder(view) } override fun getItemCount() = data.count() override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { (holder as CountViewHolder).count.apply { val item = data[position] text = item.second.toString() setOnClickListener { onCountClickAction.invoke(item) } } } class CountViewHolder(view: View) : RecyclerView.ViewHolder(view) { val count: TextView = view.findViewById(R.id.count) } }
I think you catch the essence - such a jumble reminds a little of a healthy development. And more and more new requirements from the customer are ahead: to fix the advertising banner at the top of the list, to realize the possibility of choosing the number of ordered ducks. Only these tasks will eventually turn into successive adapters, which will again have to be written from scratch.
The process of developing a classic adapter in githab history
In fact, the picture is not at all encouraging: individual adapters have to be sharpened for specific cases. We all understand that there are dozens or even hundreds of such screen lists in a real application. And they do not contain information about ducks, but more complex data. Yes, and their design is much more complicated.
Imagine the development of the application for years ahead is unrealistic, and meaningless. After a couple of such dances with a tambourine as in the last chapter and writing dozens of adapters, anyone will have the question “Maybe there are other solutions?”.
Having protested Github, we find out that the first AdapterDelegates library appeared in 2015, and a year later, Groupie and Epoxy added to the arsenal of developers - they all help make life easier, but each has its own specifics and pitfalls.
There are several other similar libraries (for example, FastAdapter), but neither I nor my colleagues worked with them, so we will not discuss them in the article.
Before comparing libraries, we briefly analyze the above case with an online store, provided the AdapterDelegates is used - from the libraries being parsed it is the easiest from the point of view of internal implementation and use (however, it is not completely advanced, so much has to be written by hand).
The library does not completely save us from the adapter, but it will be formed from blocks (bricks), which we can safely add to the list or remove from it and change their places.
class DucksDelegatesAdapter : ListDelegationAdapter<List<DisplayableItem>>() { init { delegatesManager.addDelegate(RubberDuckDelegate()) } fun setData(items: List<DisplayableItem>) { this.items = items notifyDataSetChanged() } } private class RubberDuckDelegate : AbsListItemAdapterDelegate<RubberDuckItem, DisplayableItem, RubberDuckDelegate.ViewHolder>() { override fun isForViewType(item: DisplayableItem, items: List<DisplayableItem>, position: Int): Boolean { return item is RubberDuckItem } override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { val item = parent.context.inflate(R.layout.item_rubber_duck, parent, false) return ViewHolder(item) } override fun onBindViewHolder(item: RubberDuckItem, viewHolder: ViewHolder, payloads: List<Any>) { viewHolder.apply { rubberDuckImage.showIcon(item.icon) } } class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val rubberDuckImage: ImageView = itemView.findViewById(R.id.rubberDuckImage) } }
class DucksDelegatesActivity : AppCompatActivity() { private lateinit var ducksList: RecyclerView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_ducks_delegates) ducksList = findViewById(R.id.duckList) ducksList.apply { layoutManager = LinearLayoutManager(this@DucksDelegatesActivity) adapter = createAdapter().apply { showData() } } } fun createAdapter(): DucksDelegatesAdapter { return DucksDelegatesAdapter() } private fun DucksDelegatesAdapter.showData() { setData(getRubberDucks()) } private fun getRubberDucks(): List<DisplayableItem> { return DuckMockData.ducks.orEmpty().map { RubberDuckItem(it.icon) } } }
Already from the first task we see the difference: we have an adapter class that is inherited from the library. And in addition - the very brick, which is called the delegate and from which we also inherit and implement part of the logic we need. Next we add a delegate to the manager - this is also a library class. And the last thing to do is create an adapter and fill it with data.
To implement the second category of store and headers, we will write a couple of delegates, and the animation comes from the DiffUtil class.
Here I’ll mark a brief but categorical conclusion: the use of even this library solves all the listed problems that arose when the application became more complex in a case with an online store, but without the drawbacks anywhere, and more about them.
Adapter development process with AdapterDelegates in github history
Immerse more in the functionality and operation of each of the libraries. I somehow applied all three libraries on our projects, depending on the tasks and complexity of the application.
We use this library in the application of one of the largest Russian airlines. We needed to replace the simple pay list with a list with groups and a large number of different parameters.
Simplified scheme of the library looks like this:
The main class is DelegateAdapter , the various “bricks” are the “delegates” who are responsible for displaying a particular data type and, of course, the list itself.
In general, this library solves all the main difficulties in expanding the functionality of the application and is suitable for those who have not used the library before. But I don’t recommend to dwell only on it.
Groupie , created a few years ago by Lisa Wray , we often use, including completely using it, we wrote an application for one Latvian bank.
In order to use this library, first of all you need to understand the dependencies . In addition to the main one, you can use several options to choose from:
We stop at one and write the necessary dependencies.
Using the example of an online store with ducks, we need to create an Item inherited from the library class, specify the layout, and implement the binding via Kotlin syntenty. If you compare it with the amount of code that you had to write with the AdapterDelegates , it’s just heaven and earth.
All that remains is to set the RecyclerView GroupieAdapter as an adapter, and put matched items into it.
It is seen that the scheme of work is more and more difficult. Here, in addition to simple items, you can use whole sections - groups of items and other classes.
It is important that Groupie, with all its drawbacks, is able to easily replace AdapterDelegates , especially if you plan to make folding first-level lists, and do not want to write a lot of boilerplate.
Implementing a duck list using Groupie
The last library that we started to use relatively recently is Epoxy , developed by the guys from Airbnb . The library is complex, but it allows you to solve a whole task load. Airbnb programmers themselves use it to render screens directly from the server. We Epoxy handy on one of the most recent projects - an application for the bank in Yekaterinburg.
To develop screens, we had to work with different types of data, a huge number of lists. And one of the screens was really endless. And Epoxy helped us all with this.
The principle of the library as a whole is similar to the two previous ones, except that instead of the adapter, the EpoxyController is used to build the list, which allows you to define the adapter structure declaratively.
To achieve this, the library is built on code generation. How it works - with all the nuances well described in the wiki and reflected in the samples .
Implementing a list of ducks using Epoxy
The main thing that I wanted to convey: you should not put up with the complexity that appears when you need to make complex lists and constantly have to redo them. And this happens very often. And in principle, when they are implemented, if the project only starts, or you are engaged in its refactoring.
The reality is that you shouldn’t complicate the logic once again, thinking that there’s enough of some kind of own abstractions. They are not long enough. But working with them is not only not enjoyable, it also remains a temptation to transfer part of the logic to the UI-part, which should not be there. There are tools that will help avoid most problems, and they need to be used.
I understand that for many experienced (and not only) developers, this is either obvious, or they may not agree with me. But I consider it important to re-emphasize this.
It is rather difficult to advise on one library, because the choice depends on many factors: from personal preferences to ideology on the project.
I would do the following:
If you have any questions, you can look at the link to once again see the code of our application with the ducks.
Source: https://habr.com/ru/post/428525/
All Articles