RecyclerView is a really useful way of displaying content in list form, particularly when the content is dynamic and / or there are large numbers of items. One thing that can be really useful is that we get some really nice animations for free provided we implement our Adapter correctly. For those that have converted from ListView there is a tendency to follow the same usage patterns when updating the data, but this will not get the best out of RecyclerView. In this short series we’ll take a look at the right way to modify the contents of a RecyclerView.Adapter in order to get these animations for free.



Let’s begin by creating a simple RecyclerView implementation. We’ll have a simple text item which will have some buttons to add and remove items. The add button will insert a new item at the current position, and the remove button will remove the current item. There is also a button in the ActionBar to append a new item – this is useful when we have no items in the list. Much of the code is fairly standard, so I won’t bother displaying it here. The important part is the Adapter:

MyAdapter.kt class MyAdapter(private val string: String) : RecyclerView.Adapter<MyAdapter.ViewHolder>() { private val items: MutableList<String> = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = LayoutInflater.from(parent.context) .inflate(R.layout.list_item, parent, false) .run { ViewHolder(this) } override fun getItemCount(): Int = items.size fun appendItem(newString: String) = items.add(uniqueString(newString)).also { notifyDataSetChanged() } override fun onBindViewHolder(holder: ViewHolder, position: Int) { with(holder) { bind(items[position]) } } private fun uniqueString(base: String) = "$base ${(Math.random() * 1000).toInt()}" inner class ViewHolder( itemView: View, private val textView: TextView = itemView.findViewById(android.R.id.text1), upButton: View = itemView.findViewById(R.id.up), downButton: View = itemView.findViewById(R.id.down) ) : RecyclerView.ViewHolder(itemView) { init { addButton.setOnClickListener(insert()) removeButton.setOnClickListener(remove()) } private fun insert(): (View) -> Unit = { layoutPosition.also { currentPosition -> items.add(currentPosition, uniqueString(string)) notifyDataSetChanged() } } private fun remove(): (View) -> Unit = { layoutPosition.also { currentPosition -> items.removeAt(currentPosition) notifyDataSetChanged() } } fun bind(text: String) { textView.text = text } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 class MyAdapter ( private val string : String ) : RecyclerView . Adapter < MyAdapter . ViewHolder > ( ) { private val items : MutableList < String > = mutableListOf ( ) override fun onCreateViewHolder ( parent : ViewGroup , viewType : Int ) : ViewHolder = LayoutInflater . from ( parent . context ) . inflate ( R . layout . list_item , parent , false ) . run { ViewHolder ( this ) } override fun getItemCount ( ) : Int = items . size fun appendItem ( newString : String ) = items . add ( uniqueString ( newString ) ) . also { notifyDataSetChanged ( ) } override fun onBindViewHolder ( holder : ViewHolder , position : Int ) { with ( holder ) { bind ( items [ position ] ) } } private fun uniqueString ( base : String ) = "$base ${(Math.random() * 1000).toInt()}" inner class ViewHolder ( itemView : View , private val textView : TextView = itemView . findViewById ( android . R . id . text1 ) , upButton : View = itemView . findViewById ( R . id . up ) , downButton : View = itemView . findViewById ( R . id . down ) ) : RecyclerView . ViewHolder ( itemView ) { init { addButton . setOnClickListener ( insert ( ) ) removeButton . setOnClickListener ( remove ( ) ) } private fun insert ( ) : ( View ) - > Unit = { layoutPosition . also { currentPosition - > items . add ( currentPosition , uniqueString ( string ) ) notifyDataSetChanged ( ) } } private fun remove ( ) : ( View ) - > Unit = { layoutPosition . also { currentPosition - > items . removeAt ( currentPosition ) notifyDataSetChanged ( ) } } fun bind ( text : String ) { textView . text = text } } }

The key parts are the highlighted sections which are the functions which handle adding new list items, and deleting them. The pattern that we need to use is to alter the underlying list in some way (in this case we add or remove String objects from the items list). Then we call notifyDataSetChanged() which informs the RecyclerView that something has changed and it updates the list. If we don’t call this then the update is not shown.

If we run this we see the basic behaviour:

We can see the basic add and remove behaviour, but no nice animations.

Although the basic principle that we’ve used here is fundamentally sound, it is something that I have seen quite often particularly from those who are experienced with using ListView. However, it does not leverage the full power of RecyclerView. However it only take a couple of minor changes to fix this. What can be improved is the call to notifyDataSetChanged() and this is why many people who convert from ListView fall in to this trap: because that’s the correct way to trigger an update of the ListView. It certainly works with RecyclerView but it’s actually quite a bad idea for a couple of reason. Firstly it is going to trigger a full refresh of all of the list items; and secondly the RecyclerView assume that everything has changed so is unable to infer any small changes which could be animated.

The fix is to be rather more specific about what has changed. In the appendItem() function we can do this instead:

MyAdapter.kt fun appendItem(newString: String) = items.add(uniqueString(newString)).also { notifyItemInserted(itemCount - 1) } 13 14 15 16 fun appendItem ( newString : String ) = items . add ( uniqueString ( newString ) ) . also { notifyItemInserted ( itemCount - 1 ) }

Rather than use notifyDataSetChanged() we instead call notifyItemInserted() with the index of the newly inserted item string. We can also do the same thing for the buttons on each item:

MyAdapter.kt private fun insert(): (View) -> Unit = { layoutPosition.also { currentPosition -> items.add(currentPosition, uniqueString(string)) notifyItemInserted(currentPosition) } } private fun remove(): (View) -> Unit = { layoutPosition.also { currentPosition -> items.removeAt(currentPosition) notifyItemRemoved(currentPosition) } } 39 40 41 42 43 44 45 46 47 48 49 50 51 private fun insert ( ) : ( View ) - > Unit = { layoutPosition . also { currentPosition - > items . add ( currentPosition , uniqueString ( string ) ) notifyItemInserted ( currentPosition ) } } private fun remove ( ) : ( View ) - > Unit = { layoutPosition . also { currentPosition - > items . removeAt ( currentPosition ) notifyItemRemoved ( currentPosition ) } }

The only difference here is that in remove() we call notifyItemRemoved() instead.

By being more specific about what has actually changed within the Adapter items the RecyclerView is not only more efficient because it only updates what has changed, but it also provides it with enough information to animate those changes:

It is worth bearing in mind that because this is based upon a support library, it is fully backward compatible. Here’s the same code running on an Jelly Bean (API16) emulator:

That is the basic principle at work: Be specific about what has changed and RecyclerView gives you a lot more, and we’ll explore this further in the next article.

The source code for this article is available here.

© 2018, Mark Allison. All rights reserved.

Related

Copyright © 2018 Styling Android. All Rights Reserved.

Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.