Displaying large lists (Kotlin)

Remember I talked about having limited resources available in mobile apps? That means we can’t just stuff all our data into a massive scroll view and hope it works. (Websites are notorious for doing just that. To make it work, your phone kills most non-essential processes when you open those websites.)

The solution is to make it look like we have an infinite list of things to display. We do this by making views only for the list items currently onscreen. When you scroll down, the View that disappears off the top of the screen is re-used to display something else, and is added just below the bottom of the screen.

In Android, RecyclerView does all the crazy logic needed to make this work efficiently. All we need to do is to provide views whenever RecyclerView needs them, and update them to display new items when asked to. There’s a bit of code we need to write, but it’s not too complicated. The performance gain is easily worth it if you have more than 15-20 items to display.

  1. Right-click on the app module in the project overview on the left and select New → Activity → Empty Activity.
    • Name it ListActivity
    • Source language should be Kotlin
    • Check the box that says “Launcher Activity” (this makes a home screen icon the for activity)
  2. In our new layout file (mine is called activity_list.xml), drag a RecyclerView into the layout designer.
  3. When asked, yes, you add the RecyclerView library to Gradle.
    If it doesn’t ask, add this in the dependencies section of Gradle scripts/build.gradle (Module: app):

    implementation 'com.android.support:recyclerview-v7:27.1.0'
  4. Add constraints to all edges of the screen. Remember, you add constraints by grabbing the handles on the sides of our view, and dragging “springs” to the corresponding edge of the screen.
  5. Remove any margins (using the Attributes panel), and set both width and height to match_constraint (if that doesn’t work, try 0dp).
  6. Give the recycler view an ID, for example recyclerView.

But what are we going to display?? “Item0/Item1/…” isn’t particularly exiting…

  1. Right-click our res folder and select New Android Resrouce File.
  2. Set the following:
    • File name: item_deadline.xml
    • Resource type: Layout
    • Root element: android.support.constraint.ConstraintLayout
    • Source set: main
    • Directory name: layout
  3. Re-create our scene with the text, grim reaper and computer guy.
    If you don’t feel like doing it all over again, switch to “Text” view using the button at the bottom and paste this:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="140dp">
    
        <TextView
            android:id="@+id/hoursLeftTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:layout_marginRight="8dp"
            android:text="Hello World!"
            android:textAppearance="@style/TextAppearance.AppCompat.Headline"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintRight_toRightOf="parent" />
    
        <ImageView
            android:id="@+id/userImageView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_marginTop="8dp"
            app:layout_constraintBottom_toTopOf="@+id/hoursLeftTextView"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_computer_work" />
    
        <ImageView
            android:id="@+id/reaperImageView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="54dp"
            android:layout_marginTop="8dp"
            app:layout_constraintBottom_toBottomOf="@+id/userImageView"
            app:layout_constraintEnd_toEndOf="@+id/userImageView"
            app:layout_constraintTop_toTopOf="@+id/userImageView"
            app:srcCompat="@drawable/ic_grim_reaper" />
    
    </android.support.constraint.ConstraintLayout>

In our code, we need to write three classes:

  • Deadline – Contains the deadline date and name
  • DeadlineViewHolder – Wrapper around an item view, takes care of switching out the contents whenever the view should display a different item.
  • DeadlineAdapter – Create views and coordinate updates

Let’s start with the simplest one:

  1. Create a new Kotlin class called Deadline:
    data class Deadline(var title: String, val date: Date) {}
  2. Copy our getHoursLeft() method into this class. (Make sure you get the variable names right.)

Hopefully not too bad? Let’s try the next one:

  1. Create a new Kotlin class called DeadlineViewHolder.
  2. It should take one constructor parameter: view: View.
  3. Extend RecyclerView.ViewHolder.
    1. Remember Kotlin uses : instead of extends
    2. You need to call the constructor right away. It takes a View, wonder which one…?

Before we finish the view holder, we need to understand how it will be used. So let’s create the adapter which will use the view holder:

Note: this section can be a bit overwhelming if you’re not used to this kind of programming. If you get stuck, look at what variables and methods you have available in the code you’re writing and what you need to end up with (the return value).

  1. Create a new Kotlin class called DeadlineAdapter.
  2. Make it extend RecyclerView.Adapter<DeadlineViewHolder>
    Hint: Remember the thing with the colon and the constructor call.
  3. There will be a red line telling you that you need to implement some methods. Click the text with the red line, hit Alt+Enter and select “Implement methods”. This should give you three methods you need to implement, press OK to add them.
    • As you see, we need to create views (more precisely, view holders), bind them (=make them display our items), and tell RecyclerView how many items we have.
  4. We don’t have any items yet, so declare a mutable property (same as “field” in Java) called items and initialize it to an empty list.
  5. When this list changes, we need to tell RecyclerView about it. The easiest way to do that, is to create a setter. Kotlin has a special syntax for that:
    var items: List<Deadline> = emptyList()
        set(value) {
            notifyDataSetChanged() // Tell RecyclerView we got some new data
            field = value // Put the value we got into the field
        }
  6. Implement getItemCount.
    Hint: it’s a one-liner. You should be able to figure it out on your own.
  7. onCreateViewHolder is more interesting. We need to parse item_deadline.xml and turn it into views somehow. In Android, this is called inflating the layout, and we do it using a layout inflater:
    // Fetch a layout inflater
    val layoutInflater = LayoutInflater.from(parent.context)
    // Read the layout for each item from the item_deadline file.
    // (Don't attach it, RecyclerView takes care of that)
    val view = layoutInflater.inflate(R.layout.item_deadline, parent, false)

    Hint: If you read the method header, you see that we should return a DeadlineViewHolder, not a View. Remember what argument the DeadlineViewHolder constructor takes? Hmm…

  8. The last method is onBindViewHolder. It gives us a viewHolder, and a position in the list. What we need to do is to fetch the corresponding deadline from the list of items, and give it to the view holder. Of course, this means that we need to add a method to the viewHolder that takes a Deadline object, let’s name it bind:
    viewHolder.bind(deadline)
  9. Click the red bind method call, press Alt+Enter and “Create member function” to create the method.

Still following? Let’s finish up our DeadlineViewHolder class now that we know where it will be called:

  1. At the top, just below the class name, create references to our views. Kotlin’s magic doesn’t work here, so we need to use findViewById manually like this:
    val hoursLeftTextView: TextView = view.findViewById(R.id.hoursLeftTextView)

    Do the same for reaperImageView.

  2. We have our empty bind(deadline: Deadline) method. This method should do essentially the same thing as updateUI() in MainActivity, so copy the contents of that method in here.
  3. Update the code to use number of hours left from the Deadline object.
  4. Make our hoursLeftTextView display the title of the deadline too.

Now we have everything we need to display a large list! All we need to do is to actually put some items in it and display it:

  1. Go to ListActivity.kt
  2. Declare a property deadlineAdapter = DeadlineAdapter()
  3. Set up the recyclerView:
    1. adapter = deadlineAdapter
    2. We want the views stacked vertically, the Linear Layout Manager does this by default:
      layoutManager = LinearLayoutManager(context)
  4. Add some deadlines to our adapter!
    • I’m lazy, so I made an extra constructor in our Deadline class:
      constructor(title: String, month: Int, date: Int) : this(title, Date()) {
          this.date.month = month - 1 // Date does 0-indexing on month numbers…
          this.date.date = date // …but not on day of month
          this.date.hours = 23
          this.date.minutes = 0
      }
    • If you happen to take the same courses as me, here are some deadlines you can use:
      val now = Date()
      deadlineAdapter.items = listOf(
              Deadline("Android-workshop", 4, 18),
              Deadline("Cogito-kurs", 4, 24),
              Deadline("DB øving 4", 4, 14),
              Deadline("PU Sprint 3", 4, 16),
              Deadline("MMI øving 5", 4, 30),
              Deadline("SL innlevering 3", 4, 30),
              Deadline("Eksamen KTN", 5, 15),
              Deadline("Eksamen SL", 5, 24),
              Deadline("Eksamen DB", 5, 30),
              Deadline("Eksamen MMI", 6, 5)
      ).filter { it.date.after(now) } // Don't display expired deadlines

LF

Congratulations, you made it to the end of this workshop! 🎉

Now what?

You can try to:

  • Sort deadlines by due date.
  • Save deadlines in SharedPreferences. You need to write code to convert the deadlines into strings and then reconstruct them from those strings. An easy way to do so is to use the JsonObject (and JsonArray maybe) class.
  • Add a “+” button so users can add new deadlines.
    • This button would open a new activity (let’s name it AddDeadlineActivity), where the user can specify date and title for the deadline. This activity would also save the new deadline in SharedPreferences.
    • Use an Intent along with startActivityForResult() to open the activity.
    • Override onActivityResult to update the items displayed in the recycler view when AddDeadlineActivity closes. From AddDeadlineActivity, you can use setResult() for indicate whether a deadline was added.
  • Let users delete deadlines by tapping and holding the computer guy image.
    • Create an interface containing only one function, onDeadlineDeleted(deadline:Deadline).
    • Modify the adapter to take this interface as a constructor parameter.
    • Use setOnLongClickListener in the code where you set up the view holder, and call onDeadlineDeleted from there.
    • Make ListActivity implement the interface somehow, and write code to modify the saved deadlines (and reload the list of course).
    • Maybe display a dialog to confirm deletion? Try AlertDialog.Builder
  • Visit the GitHub repo and send pull requests for the code you wrote in these last few steps.