This practical codelab is part of Unit 4: User experience in Mobile Development module. You will get the most value out of this course if you work through the codelabs in sequence:
For the complete list of codelabs in the course, see Blackboard.
For links to all the concept chapters, apps, and slides, see Android Developer Fundamentals (Version 2).
Letting the user display, scroll, and manipulate a list of similar data items is a common app feature. Examples of scrollable lists include contact lists, playlists, saved games, photo directories, dictionaries, shopping lists, and indexes of documents.
In the practical on scrolling views, you use ScrollView
to scroll a View
or ViewGroup
. ScrollView
is easy to use, but it's not recommended for long, scrollable lists.
RecyclerView is a subclass of ViewGroup
and is a more resource-efficient way to display scrollable lists. Instead of creating a View
for each item that may or may not be visible on the screen, RecyclerView
creates a limited number of list items and reuses them for visible content.
In this practical you do the following:
RecyclerView
to display a scrollable list.You should be able to:
View
to a string using getText()
.onClick()
handler to a View
.Toast
message.RecyclerView
class to display items in a scrollable list.RecyclerView
as they become visible through scrolling.RecyclerView
to display a list of items as a scrollable list and associate click behavior with the list items.RecyclerView
.The RecyclerView
app demonstrates how to use a RecyclerView
to display a long scrollable list of people. You create the dataset (the people), the RecyclerView
itself, and the actions the user can take:
Before you can display a RecyclerView
, you need data to display. In this task, you will create a new project for the app and a dataset. In a more sophisticated app, your data might come from internal storage (a file, SQLite database, saved preferences), from another app (Contacts, Photos), or from the internet (cloud storage, Google Sheets, or any data source with an API). Storing and retrieving data is a topic of its own covered in the data storage chapter. For this exercise, you will simulate data by creating it in a class called DataSource
.
activity_main.xml
), and a layout for the Activity content (content_main.xml). This layout contains a NavHostfragment and there are two fragments called FirstFragment
and SecondFragment
.In this step you create a LinkedList of 20 Person objects which have been generated at random. A useful source for generating random data, which I have used here, is https://github.com/mdeanda/lorem.
model
package called Person and enter the following code: public class Person {
String firstName;
String lastName;
String phone;
String email;
int clickCount;
public Person(String f, String l, String p, String e) {
this.firstName = f;
this.lastName = l;
this.phone = p;
this.email = e;
clickCount = 0;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public void incrementClickCount() {
this.clickCount++;
}
public String simpleDisplay() {
String clickMsg = "";
if (clickCount > 0) {
clickMsg = ". Clicked: " + clickCount + " times";
}
return firstName + " " + lastName + clickMsg;
}
}
Each Person has a firstName
, lastName
, phone
and email
property.model
package called DataSource and enter the following code: import java.util.LinkedList;
public class DataSource {
// testData is a stock of data items to add
private LinkedList<Person> testData;
// data will contain the list of data items displayed by the app
private LinkedList<Person> data;
private int testDataIndex = 0;
private static DataSource _instance;
private DataSource() {
data = new LinkedList<>();
testData = new LinkedList<>();
testData.add(new Person("Andre", "Snow", "(549) 413-0834", "andre.snow@example.com"));
testData.add(new Person("Maynard", "Shields", "(457) 311-9060", "maynard.shields@example.com"));
testData.add(new Person("Sherry", "Ellison", "(916) 926-6098", "sherry.ellison@example.com"));
testData.add(new Person("Lester", "Buckley", "(699) 228-0885", "lester.buckley@example.com"));
testData.add(new Person("Heriberto", "Pena", "(504) 512-6981", "heriberto.pena@example.com"));
testData.add(new Person("Hugh", "Coffey", "(446) 273-9747", "hugh.coffey@example.com"));
testData.add(new Person("Delmar", "Roach", "(730) 122-4126", "delmar.roach@example.com"));
testData.add(new Person("Ivy", "Bishop", "(765) 253-6871", "ivy.bishop@example.com"));
testData.add(new Person("Otis", "Estes", "(460) 524-2120", "otis.estes@example.com"));
testData.add(new Person("Clayton", "Good", "(349) 700-9200", "clayton.good@example.com"));
testData.add(new Person("Zachary", "Johns", "(775) 689-1912", "zachary.johns@example.com"));
testData.add(new Person("Dionne", "Wolf", "(137) 269-4844", "dionne.wolf@example.com"));
testData.add(new Person("Gena", "Barton", "(524) 729-4493", "gena.barton@example.com"));
testData.add(new Person("Wade", "Johnson", "(711) 885-7832", "wade.johnson@example.com"));
testData.add(new Person("Glenda", "Briggs", "(984) 407-9968", "glenda.briggs@example.com"));
testData.add(new Person("Mercedes", "Joseph", "(551) 170-0298", "mercedes.joseph@example.com"));
testData.add(new Person("Catherine", "Calderon", "(831) 428-5718", "catherine.calderon@example.com"));
testData.add(new Person("Adan", "Rodriquez", "(519) 708-9057", "adan.rodriquez@example.com"));
testData.add(new Person("Pierre", "Lindsay", "(905) 407-3679", "pierre.lindsay@example.com"));
testData.add(new Person("Brandon", "Morris", "(110) 702-1306", "brandon.morris@example.com"));
for (int i = 0; i< 5; i++) {
addPerson();
}
}
public static DataSource getInstance() {
if (_instance == null) {
_instance = new DataSource();
}
return _instance;
}
public void addPerson() {
// copy a Person from testData to data
data.add(testData.get(testDataIndex));
// update to the next index, reset to 0 if at the end of the list
// some Person objects may be added twice if you add a lot of data
testDataIndex = (testDataIndex + 1) % testData.size();
}
public LinkedList<Person> getData() {
return data;
}
}
This class follows the singleton design pattern and has a method that will return a LinkedList<Person>
of data. Our DataSource
keeps a stock of data in a LinkedList
called testData
and will copy items from that list into the LinkedList
called data
which will contain the actual data displayed by the app. Adding a new data item will simply copy the next Person
from testdata
into data
. Our testData
acts as a mock source of real data.
private final LinkedList mPersonList;
public FirstFragment() {
super();
mPersonList = DataSource.getInstance().getData();
}
For this practical, you will use a FAB to generate a new Person to insert into the list. The Basic Activity template provides a FAB, but you may want to change its icon. As you learned in another lesson, you can choose an icon from the set of icons in Android Studio for the FAB. Follow these steps:
In this practical, you display data in a RecyclerView
. You need the following:
DataSource
.RecyclerView
for the scrolling list that contains the list items.View
elements. RecyclerView
requires an explicit layout manager to manage the arrangement of list items contained within it. This layout could be vertical, horizontal, or a grid. You will use a vertical LinearLayoutManager.RecyclerView
. It prepares the data in a RecyclerView.ViewHolder. You will create an adapter that inserts into and updates your generated data in your views.ViewHolder
that contains the View
information for displaying one item from the item's layout.The diagram below shows the relationship between the data, the adapter, the ViewHolder
, and the layout manager.
To implement these pieces, you will need to:
RecyclerView
element to the FirstFragment
XML content layout (fragment_first.xml
) for the RecyclerViews app.personlist_item.xml
) for one list item, which is a PersonListItem
.PersonListAdapter
) with a ViewHolder
(PersonViewHolder
). Implement the method that takes the data, places it in the ViewHolder
, and lets the layout manager know to display it.onViewCreated()
method of FirstFragment
, create a RecyclerView
and initialize it with the adapter and a standard layout manager.Let's do these one at a time.
To add a RecyclerView
element to the XML layout, follow these steps:
Button
at the center of a ConstraintLayout
.TextView
and Button
elements with the following: <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
You need to specify the full path (androidx.recyclerview.widget.RecyclerView), because RecyclerView
is part of the Androidx Library.The adapter needs the layout for one item in the list. All the items use the same layout. You need to specify that list item layout in a separate layout resource file, because it is used by the adapter, separately from the RecyclerView
.
Create a simple Person item layout using a vertical LinearLayout
with a TextView
:
ConstraintLayout
that was created with the file with a LinearLayout
using the following code: <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="6dp">
</LinearLayout>
<TextView
android:id="@+id/person"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textStyle="bold"/>
You can use styles to allow elements to share groups of display attributes. An easy way to create a style is to extract the style of a UI element that you already created. To extract the style information for the word TextView
in personlist_item.xml
:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="6dp">
<TextView
android:id="@+id/person"
style="@style/person_title" />
</LinearLayout>
Android uses adapters (from the Adapter class) to connect data with View
items in a list. There are many different kinds of adapters available, and you can also write custom adapters. In this task you will create an adapter that associates your list of words with word list View
items.
To connect data with View
items, the adapter needs to know about the View
items. The adapter uses a ViewHolder that describes a View
item and its position within the RecyclerView
.
First, you will build an adapter that bridges the gap between the data in your person list and the RecyclerView
that displays it:
PersonListAdapter
the following signature: public class PersonListAdapter extends
RecyclerView.Adapter<PersonListAdapter.PersonViewHolder> {
}
PersonListAdapter
extends a generic adapter for RecyclerView
to use a View
holder that is specific for your app and defined inside PersonListAdapter
. PersonViewHolder
shows an error, because you have not yet defined it.OK
.Android Studio creates empty placeholders for all the methods. Note how onCreateViewHolder
and onBindViewHolder
both reference the PersonViewHolder
, which hasn't been implemented yet.
To create the ViewHolder
, follow these steps:
PersonListAdapter
class, add a new WordViewHolder
inner class with this signature: class PersonViewHolder extends RecyclerView.ViewHolder {}
You will see an error about a missing default constructor. You can see details about the errors by hovering your mouse cursor over the red-underlined code or over any red horizontal line on the right margin of the editor pane.PersonViewHolder
inner class for the TextView
and the adapter: public final TextView personItemView;
final PersonListAdapter mAdapter;
WordViewHolder
, add a constructor that initializes the ViewHolder
TextView
from the person
XML resource, and sets its adapter: public PersonViewHolder(View itemView, PersonListAdapter adapter) {
super(itemView);
personItemView = itemView.findViewById(R.id.person);
this.mAdapter = adapter;
}
FirstFragment.java
. Open FirstFragment.java and edit the onViewCreated method to delete the code after the call to the super method. After you have done that, the method should look like this: public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
You need to hold your data in the adapter, and PersonListAdapter needs a constructor that initializes the word list from the data. Follow these steps:
private LinkedList<Person> mPersonList;
@Override
public int getItemCount() {
return mPersonList.size();
}
PersonListAdapter
needs a constructor that initializes the person list from the data. To create a View
for a list item, the PersonListAdapter
needs to inflate the XML for a list item. You use a layout inflator for that job. LayoutInflator
reads a layout XML description and converts it into the corresponding View
items. Start by creating a member variable for the inflater in PersonListAdapter
: private LayoutInflater mInflater;
PersonListAdapter
. The constructor needs to have a context parameter, and a linked list of Person
with the app's data. The method needs to instantiate a LayoutInflator
for mInflater
and set mPersonList
to the passed in data: public PersonListAdapter(Context context,
LinkedList<Person> personList) {
mInflater = LayoutInflater.from(context);
this.mPersonList = personList;
}
onCreateViewHolder()
method and complete it with this code: @Override
public PersonListAdapter.PersonViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
View mItemView = mInflater.inflate(R.layout.personlist_item,
parent, false);
return new PersonViewHolder(mItemView, this);
}
The onCreateViewHolder()
method is similar to the onCreate()
method. It inflates the item layout, and returns a ViewHolder
with the layout and the adapter.onBindViewHolder()
method and complete it with the code below: @Override
public void onBindViewHolder(@NonNull PersonListAdapter.PersonViewHolder holder,
int position) {
Person person = mPersonList.get(position);
String mCurrent = person.simpleDisplay();
holder.personItemView.setText(mCurrent);
}
The onBindViewHolder()
method connects your data to the view holder.Now that you have an adapter with a ViewHolder
, you can finally create a RecyclerView
and connect all the pieces to display your data.
private RecyclerView mRecyclerView;
private PersonListAdapter mAdapter;
onViewCreated()
method of FirstFragment
, add the following code (after the call to super.onViewCreated
) that creates the RecyclerView
and connects it with an adapter and the data. The comments explain each line. // Get a handle to the RecyclerView.
mRecyclerView = view.findViewById(R.id.recyclerview);
// Create an adapter and supply the data to be displayed.
mAdapter = new PersonListAdapter(getContext(), mPersonList);
// Connect the adapter with the RecyclerView.
mRecyclerView.setAdapter(mAdapter);
// Give the RecyclerView a default layout manager.
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
Looking at lists of items is interesting, but it's a lot more fun and useful if your user can interact with them. To see how the RecyclerView
can respond to user input, you will attach a click handler to each item. When the item is tapped, the click handler is executed, and the full contact details of that person will be displayed in the second fragment.
The list of items that a RecyclerView
displays can also be modified dynamically—it doesn't have to be a static list. There are several ways to add additional behaviors. One is to use the floating action button (FAB). For example, in Gmail, the FAB is used to compose a new email. For this practical, you will generate a new contact to insert into the list.
class PersonViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener {
onClick()
method.// Get the position of the item that was clicked.
int mPosition = getLayoutPosition();
// Use that to access the affected item in mPersonList.
Person p = mPersonList.get(mPosition);
// Register the click on the person
p.incrementClickCount();
// Notify the adapter, that the data has changed so it can
// update the RecyclerView to display the data.
mAdapter.notifyDataSetChanged();
onClickListener
with the View
. Add this code to the PersonViewHolder
constructor (below the this.mAdapter = adapter
line): itemView.setOnClickListener(this);
In this task you will implement an action for the FAB to:
Follow these steps:
MainActivity
. We can get access to the FAB once this fragment has started. Add the following onStart
lifecycle method to your FirstFragment
: @Override
public void onStart() {
super.onStart();
getActivity().findViewById(R.id.fab).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DataSource.getInstance().addPerson();
mAdapter.notifyDataSetChanged();
}
});
}
This method will gain a reference to the FAB from the MainActivity
and add an OnClickListener
. The listener has a single method called onClick
which will tell our DataSource
to add a Person
to the list and then notify our adapter that its data set has changed. The call to notifyDataSetChanged
is important because otherwise the view will not be updated and the newly added data will not be seen.Challenge 1: Change the options menu to show only one option: Reset. This option should return the list of people to its original state of five people, with nothing clicked and no extra people.
Challenge 2: Creating a click listener for each item in the list is easy, but it can hurt the performance of your app if you have a lot of data. Research how you could implement this more efficiently. This is an advanced challenge. Start by thinking about it conceptually, and then search for an implementation example
View
for each list item, the adapter inflates an XML layout resource for a list item using LayoutInflator.RecyclerView
layout manager that shows items in a vertical or horizontal scrolling list.RecyclerView
layout manager that shows items in a gridRecyclerView
layout manager that shows items in a staggered grid.RecyclerView
. It prepares the data in a RecyclerView.ViewHolder that describes a View
item and its position within the RecyclerView
.The related concept documentation is 4.5: RecyclerView.
Android Studio documentation:
Android developer documentation:
RecyclerView Animations and Behind the Scenes (Android Dev Summit 2015)