Getting started with RecyclerView, ListAdapter and LayoutAnimation

RecyclerView is a flexible and upgraded version of ListView. In short, RecyclerView works on ViewHolder design pattern. Each row managed by ViewHolder.

Dependencies

dependencies {
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha05"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
}

Model

data class Movie(var title: String, var rating: Float)

Basic kind of Adapter and Holder

Every RecyclerView has a corresponding Adapter. The Adapter is responsible to take our models and map them each to their corresponding View components (ViewHolders).

Creating of Adapter consists of following steps

  1. Set Movie models in the constructor or a setter method.
  2. Tell the Adapter how many items it has at any given time by overriding getItemCount.
  3. Map the data model to the corresponding view type by overriding getItemViewType.
  4. Based on the view type, create the corresponding ViewHolder by overriding onCreateViewHolder.
  5. Bind ViewHolder with the data model by overriding onBindViewHolder.
  6. Create multiple ViewHolders and implement the View logic for each use case using the data model provided.
class MovieAdapter(var items: List<Movie>): RecyclerView.Adapter<MainAdapter.MovieHolder>() {
    override fun getItemCount() = items.size
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = 
        MovieHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false))
    override fun onBindViewHolder(holder: MovieHolder, position: Int) { holder.bind(items[position]) }

    inner class MovieHolder(override val containerView: View): 
        RecyclerView.ViewHolder(containerView), LayoutContainer {
        fun bind(item: Movie) {
            tvTitle.text = item.title
            tvRating.text = item.rating
        }
    }
}

Activity class

class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        lifecycleScope.launch(Dispatchers.IO) {
            val response = Repository.getMovies()
            if (response.isSuccessful) {
                val adapter = MainAdapter(response.body() ?: listOf())
                recycler.adapter = adapter
                ...
            } else {
                Toast.makeText(this@MainActivity, "Error ${response.code()}", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

ListAdapter

ListAdapter is a new class bundled in the 27.1.0 support library and simplifies the code required to work with RecyclerViews. The end result is less code for you to write and more recycler view animations happening for free. It automatically stores the previous list of items and utilises DiffUtil under the hood to only update items in the recycler view which have changed. This typically means better performance as you avoid refreshing the whole list, and nicer animations because only items which change need to be redrawn.

To display items on RecyclerView you need to the following:

  • RecyclerView widget added to the activity layout.
  • A class extending DiffUtil.ItemCallback. DiffUtil is a utility class that can calculate the difference between two lists and output a list of update operations that converts the first list into the second one.
  • A class extending ListAdapter.
  • A class extending RecyclerView.ViewHolder.
  • Layout for RecyclerView items.
class MovieAdapter(private val listItemClickListener: ListItemClickListener)
    : ListAdapter<Movie, RecyclerView.ViewHolder>(ListItemCallback()) {

    interface ListItemClickListener {
        fun onItemClick(item: Movie, position: Int)
    }

    class ListItemCallback : DiffUtil.ItemCallback<Movie>() {
        override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
            return oldItem == newItem
        }

        override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
            return oldItem.title == newItem.title && oldItem.rating == newItem.rating
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val item = getItem(position)
        (holder as ListenItemViewHolder).bind(item, position)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
        return ListenItemViewHolder(view)
    }

    inner class ListenItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var tvTitle: TextView = itemView.tvTitle
        var tvRating: TextView = itemView.tvRating

        fun bind(item: Movie, position : Int) {
            tvTitle.text = item.title
            tvRating.text = "Rating: ${item.rating}"

            itemView.setOnClickListener {
                listItemClickListener.onItemClick(item, adapterPosition)
            }
        }
    }
}

Activity class

class MainActivity : AppCompatActivity(), MovieAdapter.ListItemClickListener {
    private val movies = ArrayList<Movie>()
    private lateinit var movieAdapter: MovieAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        movieAdapter = MovieAdapter(this)
        rvItems.layoutManager = LinearLayoutManager(this)
        rvItems.itemAnimator = DefaultItemAnimator()
        rvItems.adapter = movieAdapter
        rvItems.layoutAnimation = AnimationUtils.loadLayoutAnimation(this, 
            R.anim.layout_animation_right_to_left)
        (0..99).mapTo(movies) { Movie("Movie $it", it.toFloat()) }
        movieAdapter.submitList(movies)

        fab.setOnClickListener {
            val newList = ArrayList<Movie>()
            val end = (0..10).random()
            (0..end).mapTo(newList) { Movie("New movie $it", it.toFloat()) }
            movieAdapter.submitList(newList)
        }
    }

    override fun onItemClick(item: Movie, position: Int) {
        Log.d("TAG", "item: $item");
    }
}

activity_main.xml

<?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=".view.activities.MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvItems"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layoutAnimation="@anim/layout_animation_right_to_left"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:listitem="@layout/list_item" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|right"
            android:layout_margin="16dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:srcCompat="@android:drawable/ic_menu_add"/>

</androidx.constraintlayout.widget.ConstraintLayout>

list_item.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        style="@style/TextAppearance.AppCompat.Large"/>

    <TextView
        android:id="@+id/tvRating"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        style="@style/TextAppearance.AppCompat.Small"/>
</LinearLayout>

LayoutAnimation

LayoutAnimation adds an initial content animation for a RecyclerView. So let’s start of by creating the item animation, in this example we’ll go for the Fall Down animation shown below.

Start of by creating the file item_animation_fall_down.xml in res/anim/ and add the following:

<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:duration="@integer/anim_duration_medium">

    <translate
        android:fromYDelta="-20%"
        android:toYDelta="0"
        android:interpolator="@android:anim/decelerate_interpolator"/>
    <alpha
        android:fromAlpha="0"
        android:toAlpha="1"
        android:interpolator="@android:anim/decelerate_interpolator"/>
    <scale
        android:fromXScale="105%"
        android:fromYScale="105%"
        android:toXScale="100%"
        android:toYScale="100%"
        android:pivotX="50%"
        android:pivotY="50%"
        android:interpolator="@android:anim/decelerate_interpolator"/>
</set>

With the item animation done it’s time to define the layout animation which will apply the item animation to each child in the layout. Create a new file called layout_animation_fall_down.xml in res/anim/ and add the following:

<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:animation="@anim/item_animation_fall_down"
    android:delay="15%"
    android:animationOrder="normal"/>

A LayoutAnimation can be applied programmatically in the following way:

val resId = R.anim.layout_animation_fall_down
val animation = AnimationUtils.loadLayoutAnimation(ctx, resId)
recyclerview.setLayoutAnimation(animation)

We can set the LayoutAnimation on RecyclerView in XML in the following way:

<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="match_parent"                                        
    android:layoutAnimation="@anim/layout_animation_fall_down"/>

If you are changing data set or just want to re-run the animation you can do it like this:

fun runLayoutAnimation(recyclerView: RecyclerView) {
    val context = recyclerView.getContext()
    val animation =
        AnimationUtils.loadLayoutAnimation(context, R.anim.layout_animation_fall_down)

    recyclerView.setLayoutAnimation(animation)
    recyclerView.getAdapter().notifyDataSetChanged()
    recyclerView.scheduleLayoutAnimation()
}

Useful links