Intermediate RecyclerView Tutorial with Kotlin

In this RecyclerView tutorial you will learn how to build out intermediate techniques like swiping, animation and filtering in Kotlin. By Kevin D Moore.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Adapter

Your adapter will extend the RecyclerView.Adapter class and use DefaultViewHolder. Navigate to the com.raywenderlich.marsrovers.recyclerview package and add a new Kotlin class called PhotoAdapter.

The class will start out like so:

class PhotoAdapter(private var photoList: ArrayList<PhotoRow>) : RecyclerView.Adapter<DefaultViewHolder>() {

Along with the passed in list of photos, create two more variables at the beginning of the class:

private var filteredPhotos = ArrayList<PhotoRow>()
private var filtering = false

The filterPhotos list is used to hold photos for a specific camera, and the filtering flag will be true when the user is filtering.

There are three abstract methods of RecyclerView.Adapter that have to be implemented: getItemCount, onCreateViewHolder, and onBindViewHolder. You will also override the getItemViewType method to return different values for the header and photo row type.

getItemCount returns the number of photos available. If filtering is on, return the size from the filtered list:

override fun getItemCount(): Int {
 if (filtering) {
     return filteredPhotos.size
 }
 return photoList.size
}

onBindViewHolder is where you load the photo or set the header text.

override fun onBindViewHolder(holder: DefaultViewHolder, position: Int) {
  val photoRow : PhotoRow = if (filtering) {
    filteredPhotos[position]
  } else {
    photoList[position]
  }
  if (photoRow.type == RowType.PHOTO) {
    val photo = photoRow.photo
    Glide.with(holder.itemView.context)
        .load(photo?.img_src)
        .into(holder.getImage(R.id.camera_image))
    photo?.earth_date?.let { holder.setText(R.id.date, it) }
  } else {
    photoRow.header?.let { holder.setText(R.id.header_text, it) }
  }
}

You can see that you’re using Glide to load images into the ImageView. Glide seemed to work better for all of the Mars photos than Picasso, which was only able to load some of the images.

onCreateViewHolder is where you inflate the layout and return the ViewHolder:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DefaultViewHolder {
  val layoutInflater = LayoutInflater.from(parent.context)

  val inflatedView : View = when (viewType) {
    RowType.PHOTO.ordinal -> layoutInflater.inflate(R.layout.row_item, parent,false)
    else -> layoutInflater.inflate(R.layout.header_item, parent,false)
  }
  return DefaultViewHolder(inflatedView)
}

For the two methods onCreateViewHolder and onBindViewHolder, you need to distinguish between a header and photo row. You can do that by checking the PhotoRow’s type, as you’ll see in the next section.

Section Headers

To provide headers for rows, you just need to have different row types. This is done by letting the RecyclerView know what type to use for each row.

Override the getItemViewType method and return a different integer for each type. You will be returning two different types, one for the header and one for the photo. You can use the ordinal of the enum (so the returned values will be 0 and 1). Add the following method after onCreateViewHolder.

override fun getItemViewType(position: Int) =
  if (filtering) {
    filteredPhotos[position].type.ordinal
  } else {
    photoList[position].type.ordinal
  }

Both getItemCount and getItemViewType need to take into account the filtering flags to use either the original photoList or the filteredPhotos list.

In onCreateViewHolder, you load in a row_item layout for photos and a head_item layout for the header. The onBindViewHolder checks the type and binds the appropriate items to the ViewHolder.

Now run the app to make sure it builds. Since you haven’t added the adapter to the RecyclerView yet, you won’t see anything quite yet, only the spinning ProgressBar.

DiffUtil

DiffUtil is a utility class from the RecyclerView support library used to calculate the difference between two lists and create the operations that will morph one list into another. It will be used by the RecyclerView.Adapter to trigger the optimal data change notifications that are used to animate the RecyclerView‘s rows.

To use this method, you need to implement the DiffUtil.Callback. Add this to the end of the PhotoAdapter class:

class PhotoRowDiffCallback(private val newRows : List<PhotoRow>, private val oldRows : List<PhotoRow>) : DiffUtil.Callback() {
  override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    val oldRow = oldRows[oldItemPosition]
    val newRow = newRows[newItemPosition]
    return oldRow.type == newRow.type
  }

  override fun getOldListSize(): Int = oldRows.size

  override fun getNewListSize(): Int = newRows.size

  override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    val oldRow = oldRows[oldItemPosition]
    val newRow = newRows[newItemPosition]
    return oldRow == newRow
  }
}

This class checks the items to see if they are the same type or have the same values. The areItemsTheSame method just checks the row type but the areContentsTheSame checks to see if the rows are equal.

Additional Methods

To update the photo list, you need to pass in a new list, calculate the difference between the two lists and clear the filter. Add the following methods after getItemViewType in PhotoAdapter to support clearing the filter list, use DiffUtil to tell the Adapter how to update the photo views, and remove rows:

private fun clearFilter() {
    filtering = false
    filteredPhotos.clear()
  }

fun updatePhotos(photos : ArrayList<PhotoRow>) {
   DiffUtil.calculateDiff(PhotoRowDiffCallback(photos, photoList), false).dispatchUpdatesTo(this)
   photoList = photos
   clearFilter()
}

fun removeRow(row : Int) {
   if (filtering) {
       filteredPhotos.removeAt(row)
   } else {
       photoList.removeAt(row)
   }
   notifyItemRemoved(row)
}

Notice the notifyItemRemoved method. That method will allow animations to occur for the rows around the deleted row because it tells RecyclerView exactly how the data in the adapter has changed. It’s best not to use notifyDataSetChanged for this case, as that does not provide RecyclerView with details about exactly what has changed.

Retrofit

To get the data from NASA, you’ll be using the Retrofit and Moshi libraries. You’ll use Retrofit for downloading the data and Moshi for converting it from JSON to our models.

First, create a service interface. Create a new package named service and then right click to create a new Kotlin interface named NasaApi. Replace the code with the following:

interface NasaApi {
   @GET("mars-photos/api/v1/rovers/{rover}/photos?sol=1000&api_key=<key>")
   fun getPhotos(@Path("rover") rover: String) : Call<PhotoList>
}

Substitute your key from the NASA site for <key>. This sets up the method to get the list of Photos. The passed in rover string will be substitued for {rover}.

If you need to add an import for Call, be sure to use the one from the retrofit2 package.

Next, you’ll need to create the actual service. Your service should be a Singleton and in Kotlin, creating one is extremely easy.

Right click on the service package and select New/Kotlin File/Class, name it NasaPhotos, and change the Kind to Object. That’s it! You now have a Kotlin Singleton.

Create a variable named service that is used in the getPhotos method:

object NasaPhotos {
  private val service : NasaApi

And then add an init method. This will create the instance of Retrofit, set Moshi as the JSON converter, and finally create the service object:

init {
    val retrofit = Retrofit.Builder()
       .baseUrl("https://api.nasa.gov/")
       .addConverterFactory(MoshiConverterFactory.create())
       .build()
    service = retrofit.create(NasaApi::class.java)
}

Then, create a new method to make the call for the photos:

fun getPhotos(rover: String) : Call<PhotoList> = service.getPhotos(rover)

You’re almost there. You just need to setup the spinners and the RecyclerView adapter, which you’ll do next.