Android Basics: RecyclerView - I, creating a RecyclerView with custom Adapter
May 25, 2020
Tags: Android, Android Basics, Github Repository App, RecyclerView,
In this article I go over how to create a RecyclerView in Java. If you want to learn how to do it in Kotlin go to How to create a RecyclerView with custom Adapter in Kotlin.
Up till now we have created an app that can query the github API, parse the JSON and display names and description in a single TextView. You can get the code here to follow along.
Why are we using RecyclerView and what problem does it solve for us?
- It gives us ability to use complex layout for items in the list, there is only so you can do with appending in a TextView.
- It allows us to display our list vertically, horizontally, in a grid or in our own custom layout.
- It does a lot of optimizations, but mainly it recycles views in the list as the name suggests. For example if we have 100 items, only 10 list items are used and displayed out of which 7 are being displayed and 3 are being used for smooth scrolling. The idea here is to not generate views for all 100 items and only generate enough to cover the screen and a few extra and keep using them over and over again.
Components of a RecyclerView
A high level overview of how everything is connected
RecyclerView
: The primary view where the list is displayedAdapter
: Provides RecyclerView with new views(list items) when needed and binds data to the new and recycled views. The views are sent in aViewHolder
. The two of them can be considered sort of a single unit.ViewHolder
: Contains reference to the view objects of the item. This basically caches views objects which allows us to reuse/recycle views. When we scroll a list and an item is scrolled out of the screen, the views in that list item are bound to new data and used as a new list item, this reduces findViewById calls required for generating new views. For example if we have a list of 100 items and you see 5 items on the screen, RecyclerView will use something like 9 views in total(5 on screen, 2 above and 2 below) and keep recycling them instead of creating 100 new views.Data Source
: We could have a data source JSON/Database/Anything which is used to get data and populate our list items in our RecyclerView, the adapter pulls in data from here and binds it to the views.Layout Manager
: Manages the layout of the list items. RecyclerView prepares the list items and the layout manager manages how these items are arranged(Linear, Grid, Staggered Grid, Custom).
How do we create a RecyclerView
This is generally what the sequence would be:
- Add RecyclerView dependency.
- Add RecyclerView to the layout.
- Create layout for list items.
- Create Adapter and ViewHolder inside the adapter.
- Create Layout Manager, reference RecyclerView and create a new Adapter in the code.
- Connect Layout Manager and Adapter to the RecyclerView.
- Connect or change the data source if needed.
Adding dependency
Add the following to your dependency section in the app level build.gradle
implementation 'com.android.support:recyclerview-v7:28.0.0'
Since there will be no more releases to the support library, android studio now prompts to migrate to androidx if we add the above dependency. I used android studio’s migration and it took care of everything and changed the dependency for me.
Another way would be to set android.useAndroidX=true
and android.enableJetifier=true
in gradle.properties
and then add the following dependency:
implementation 'androidx.recyclerview:recyclerview:1.1.0'
Adding RecyclerView to the Layout
We start by removing the ScrollView and the TextView inside in the activity_main.xml
, and replace it by RecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_repo_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Creating Layout for List Items
This is the layout for each item in the list. I use LinearLayout and TextView to keep things simple. We need to display the 6 things that we store in a Repository item, so in the layout we have 6 TextViews with id’s so that we can change their text when needed. Let’s create a repository_list_item.xml
in the layout folder (res -> layout -> repositorylistitem.xml) and create a layout.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp">
<TextView
android:id="@+id/tv_repo_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"/>
<TextView
android:id="@+id/tv_repo_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stars:"/>
<TextView
android:id="@+id/tv_star_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Updated on: "/>
<TextView
android:id="@+id/tv_updated_at"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<TextView
android:id="@+id/tv_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorPrimary"/>
<TextView
android:id="@+id/tv_license"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent"/>
<View
android:layout_width="wrap_content"
android:layout_height="1dp"
android:background="#dadada"
android:layout_marginVertical="8dp"/>
</LinearLayout>
Creating a RecyclerView Adapter and ViewHolder
The adapter’s job is to provide RecyclerView with new views when needed and bind data. Now the views are provided to the RecyclerView in a ViewHolder. The ViewHolder calls findViewById to find and cache the views so that we don’t call findViewById for each data point.
My strategy is to create the ViewHolder first after we create the skeleton for our Adapter and then move on to filling the rest of the a required functions in the adapter.
public class RepositoryAdapter extends RecyclerView.Adapter<RepositoryAdapter.RepositoryAdapterViewHolder> {
private Repository[] mAllRepositories;
public static class RepositoryAdapterViewHolder extends RecyclerView.ViewHolder{
// Connects to TextViews in repository_list_items.xml
public final TextView mRepositoryName;
public final TextView mRepositoryDescription;
public final TextView mRepositoryStarCount;
public final TextView mRepositoryLanguage;
public final TextView mRepositoryUpdatedAt;
public final TextView mRepositoryLicense;
public RepositoryAdapterViewHolder(@NonNull View itemView) {
super(itemView);
this.mRepositoryName = itemView.findViewById(R.id.tv_repo_name);
this.mRepositoryDescription = itemView.findViewById(R.id.tv_repo_description);
this.mRepositoryStarCount = itemView.findViewById(R.id.tv_star_count);
this.mRepositoryLanguage = itemView.findViewById(R.id.tv_language);
this.mRepositoryUpdatedAt = itemView.findViewById(R.id.tv_updated_at);
this.mRepositoryLicense = itemView.findViewById(R.id.tv_license);
}
private void bind(Repository repo){
String name = repo.getName();
String description = repo.getDescription();
Integer stargazersCount = repo.getStargazersCount();
String language = repo.getLanguage();
String updatedAt = repo.getUpdatedAt();
String license = repo.getLicense();
mRepositoryName.setText(name);
mRepositoryStarCount.setText(String.valueOf(stargazersCount));
mRepositoryUpdatedAt.setText(updatedAt);
// Since the data in these can be null we check and bind data
// or remove the view otherwise
bindOrHideTextView(mRepositoryDescription, description);
bindOrHideTextView(mRepositoryLanguage, language);
bindOrHideTextView(mRepositoryLicense, license);
}
private void bindOrHideTextView(TextView textView, String data){
if (data == null){
textView.setVisibility(View.GONE);
}
else{
textView.setText(data);
textView.setVisibility(View.VISIBLE);
}
}
}
}
We then override the 3 methods onCreateViewHolder()
, onBindViewHolder()
, and getItemCount()
required in a RecyclerView. onCreateViewHolder()
is called to create new views. I experimented with this and in our case onCreateViewHolder()
was called only 8 times, which means for all the other items the views were recycled.
In onBindViewHolder()
we bind data to views. We can get either a new view which has no data attached to it or we could get a recycled view which has some data that we attached previously, we attach new data to the view we get. It is not required but I created a bind
method in the ViewHolder to keep things clean and call it in onBindViewHolder()
instead of binding data directly in onBindViewHolder()
.
Here I have a member variable called mAllRepositories
and it is supposed to hold all the repositories. It is used in onBindViewHolder()
, to bind views in the ViewHolder to data and in getItemCount()
to get the count of items. Since mAllRepositories
is going to be empty in the beginning, I have created the method setmAllRepositories
so that we can call the method to assign data from AsyncTask after we fetch data from the Internet.
@NonNull
@Override
public RepositoryAdapterViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.repository_list_item, parent, false);
return new RepositoryAdapterViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RepositoryAdapterViewHolder holder, int position) {
holder.bind(mAllRepositories[position]);
}
@Override
public int getItemCount() {
if (mAllRepositories == null) return 0;
return mAllRepositories.length;
}
public void setmAllRepositories(Repository[] repositories){
mAllRepositories = repositories;
notifyDataSetChanged();
}
This is what the full adapter class looks like with ViewHolder and all the methods.
public class RepositoryAdapter extends RecyclerView.Adapter<RepositoryAdapter.RepositoryAdapterViewHolder> {
private Repository[] mAllRepositories;
public static class RepositoryAdapterViewHolder extends RecyclerView.ViewHolder{
public final TextView mRepositoryName;
public final TextView mRepositoryDescription;
public final TextView mRepositoryStarCount;
public final TextView mRepositoryLanguage;
public final TextView mRepositoryUpdatedAt;
public final TextView mRepositoryLicense;
public RepositoryAdapterViewHolder(@NonNull View itemView) {
super(itemView);
this.mRepositoryName = itemView.findViewById(R.id.tv_repo_name);
this.mRepositoryDescription = itemView.findViewById(R.id.tv_repo_description);
this.mRepositoryStarCount = itemView.findViewById(R.id.tv_star_count);
this.mRepositoryLanguage = itemView.findViewById(R.id.tv_language);
this.mRepositoryUpdatedAt = itemView.findViewById(R.id.tv_updated_at);
this.mRepositoryLicense = itemView.findViewById(R.id.tv_license);
}
private void bind(Repository repo){
String name = repo.getName();
String description = repo.getDescription();
Integer stargazersCount = repo.getStargazersCount();
String language = repo.getLanguage();
String updatedAt = repo.getUpdatedAt();
String license = repo.getLicense();
mRepositoryName.setText(name);
mRepositoryStarCount.setText(String.valueOf(stargazersCount));
mRepositoryUpdatedAt.setText(updatedAt);
// Since the data in these can be null we check and bind data
// or remove the view otherwise
bindOrHideTextView(mRepositoryDescription, description);
bindOrHideTextView(mRepositoryLanguage, language);
bindOrHideTextView(mRepositoryLicense, license);
}
private void bindOrHideTextView(TextView textView, String data){
if (data == null){
textView.setVisibility(View.GONE);
}
else{
textView.setText(data);
textView.setVisibility(View.VISIBLE);
}
}
}
@NonNull
@Override
public RepositoryAdapterViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.repository_list_item, parent, false);
return new RepositoryAdapterViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RepositoryAdapterViewHolder holder, int position) {
holder.bind(mAllRepositories[position]);
}
@Override
public int getItemCount() {
if (mAllRepositories == null) return 0;
return mAllRepositories.length;
}
public void setmAllRepositories(Repository[] repositories){
mAllRepositories = repositories;
notifyDataSetChanged();
}
}
Creating and connecting Layout Manager, RecyclerView and RepositoryAdapter in the code.
We add RecyclerView and RepositoryAdapter member variables in the MainActivity
private RecyclerView mRepositoryRecyclerView;
private RepositoryAdapter mRepositoryAdapter;
and then initialize both of the above variables, create a LayoutManager and finally connect everything.
mRepositoryRecyclerView = findViewById(R.id.rv_repo_list);
mRepositoryAdapter = new RepositoryAdapter();
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(RecyclerView.VERTICAL);
mRepositoryRecyclerView.setLayoutManager(layoutManager);
mRepositoryRecyclerView.setAdapter(mRepositoryAdapter);
Connecting data fetched from the internet to the RecyclerView
We have everything setup, but we are fetching the data in the AsyncTask, but that data is not shared with the RecyclerView. This is where the method we created earlier setmAllRepositories
comes in handy. We add mRepositoryAdapter.setmAllRepositories(mRepos);
in the onPostExecute
and clean up code related to the TextView that we were using previously.
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
try {
// Get the main JSON object from the String
JSONObject json = new JSONObject(s);
// Get all the repos
JSONArray repos = json.getJSONArray("items");
mRepos = new Repository[repos.length()];
// loop every repo and add the repo array
for(int i = 0; i < repos.length(); i++){
JSONObject repo = repos.getJSONObject(i);
mRepos[i] = JSONUtils.parseRepositoryJSON(repo);
}
// Send data to the RecyclerView
mRepositoryAdapter.setmAllRepositories(mRepos);
}catch (JSONException e){
e.printStackTrace();
}
mRepositoryRecyclerView.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.GONE);
}
New Look!
And we are done! We moved from using a single TextView to using a RecyclerView. This is what our app looks like right now.
You can find the code for everything we have done in this post here.
The app looks much more organized, but we can only scroll the list right now. Let’s explore how to add click functionality to the RecyclerView list items in the next post.
Hopefully you feel more comfortable with using RecyclerView. If you find any errors, have feedback or just want to say hi please don’t hesitate to leave a comment below.