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.
- 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)
- Name it
- In our new layout file (mine is called
activity_list.xml
), drag aRecyclerView
into the layout designer. - When asked, yes, you add the
RecyclerView
library to Gradle. If it doesn’t ask, add this in thedependencies
section ofGradle scripts/build.gradle (Module: app)
:``` implementation ‘com.android.support:recyclerview-v7:27.1.0’ - 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.
- Remove any margins (using the Attributes panel), and set both width and height to
match_constraint
(if that doesn’t work, try0dp
). - Give the recycler view an ID, for example
recyclerView
.
But what are we going to display?? “Item0/Item1/…” isn’t particularly exiting…
-
Right-click our
res
folder and select New Android Resrouce File. -
Set the following:
- File name:
item_deadline.xml
- Resource type: Layout
- Root element:
android.support.constraint.ConstraintLayout
- Source set: main
- Directory name: layout
- File name:
-
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:```
<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 nameDeadlineViewHolder
– 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:
- Create a new Kotlin class called
Deadline
:``` data class Deadline(var title: String, val date: Date) {} - 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:
- Create a new Kotlin class called
DeadlineViewHolder
. - It should take one constructor parameter:
view: View
. - Extend
RecyclerView.ViewHolder
.- Remember Kotlin uses
:
instead ofextends
- You need to call the constructor right away. It takes a
View
, wonder which one…?
- Remember Kotlin uses
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).
- Create a new Kotlin class called
DeadlineAdapter
. - Make it extend
RecyclerView.Adapter<DeadlineViewHolder>
Hint: Remember the thing with the colon and the constructor call. - 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.
- As you see, we need to create views (more precisely, view holders), bind them (=make them display our items), and tell
- 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. - 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 = emptyList() set(value) { notifyDataSetChanged() // Tell RecyclerView we got some new data field = value // Put the value we got into the field } - Implement
getItemCount
. Hint: it’s a one-liner. You should be able to figure it out on your own. onCreateViewHolder
is more interesting. We need to parseitem_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 aDeadlineViewHolder
, not aView
. Remember what argument theDeadlineViewHolder
constructor takes? Hmm…- The last method is
onBindViewHolder
. It gives us aviewHolder
, and aposition
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 theviewHolder
that takes aDeadline
object, let’s name itbind
:``` viewHolder.bind(deadline) - Click the red
bind
method call, pressAlt+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:
- 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 forreaperImageView
. - We have our empty
bind(deadline: Deadline)
method. This method should do essentially the same thing asupdateUI()
inMainActivity
, so copy the contents of that method in here. - Update the code to use number of hours left from the
Deadline
object. - Make our
hoursLeftTextView
display thetitle
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:
- Go to
ListActivity.kt
- Declare a property
deadlineAdapter = DeadlineAdapter()
- Set up the
recyclerView
:adapter = deadlineAdapter
- We want the views stacked vertically, the Linear Layout Manager does this by default:
layoutManager = LinearLayoutManager(context)
- 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
- I’m lazy, so I made an extra constructor in our
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 theJsonObject
(andJsonArray
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 inSharedPreferences
. - Use an
Intent
along withstartActivityForResult()
to open the activity. - Override
onActivityResult
to update the items displayed in the recycler view whenAddDeadlineActivity
closes. FromAddDeadlineActivity
, you can usesetResult()
for indicate whether a deadline was added.
- This button would open a new activity (let’s name it
- 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 callonDeadlineDeleted
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
- Create an interface containing only one function,
- Visit the GitHub repo and send pull requests for the code you wrote in these last few steps.