Chapter 5. Lists and Grids

In this chapter, we will work with lists and grids. A list or a matrix of elements can be found in almost every app on the market. Knowing how to display a list of elements on Android is something that you learn at a basic level; however, there is a lot to expand on and understand.

It's important to know which patterns we can use here, how to recycle the view, and how to display different kinds of elements with different views in the same list.

With this in mind, we will be able to understand why RecyclerView is the successor of ListView, and we will learn how to implement a list with this component. Therefore, we will cover the following in this chapter:

  • Starting with lists
    • ListView
    • The custom adapter
    • Recycling views
    • Using the ViewHolder pattern
  • Introducing RecyclerView
    • List, grid, or stack
    • Implementation
  • OnItemClick

Starting with lists

If you have heard of RecyclerView, you might wonder why we are going through ListView. The RecyclerView widget is new; it came out with Android Lollipop, and is a revolution when displaying a list of items; it can do it vertically and horizontally, as a list or as a grid, or with nice animations among other improvements.

Answering the question, even if RecyclerView is more efficient and flexible in some scenarios, it needs extra coding to achieve the same result, so there are still reasons to use ListView. For example, there is no onItemClickListener() for item selection in RecyclerView, and there is no visual feedback when we click on an item. If we don't need customization and animations, for instance for a simple data picker popup, this could be a dialog where we just have to select a country. In this case, it's perfectly fine to use ListView rather than RecyclerView.

Another reason to start with ListView is that RecyclerView solves most of the problems presented when working with ListViews. Therefore, by starting with ListView and solving these problems, we will fully understand how RecyclerView works and why it is implemented this way. Thus, we will explain individually the patterns that are used to have a global idea of the component.

Here is an example of the basic AlertDialog with the purpose of selecting an item; here, the use of ListView makes perfect sense:

Starting with lists

Using ListViews with built-in views

When you first implement ListView, it might seem trivial and easy; however, when you spend more time with Android, you realize how complex it can get. You can very easily find performance and memory issues by just having a large list of elements with an image on every row. It can be difficult to customize the list if you try to implement a complex UI; for example, having the same list displaying different items, creating different rows with different views, or even trying to group some items while showing a section title can be a headache.

Let's start with the shortest way to implement a list, using the Android built-in item layout, which is created to be used in simple lists as discussed before. In order to show the list, we will include it in AlertDialog, which will be shown when we tap on a button in the settings fragment. I will set the text of the button to Lists Example.

The first step is to create the button in settings_fragment.xml; once created, we can set the click listener to the button. Now, we understand a bit more about software patterns instead of setting the click listener in the following way:

view.findViewById(R.id.settingsButtonListExample).setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    //Show the dialog here
  }
});

We will do it in a more structured way, especially because we know that in the settings screen, there will be a good number of buttons, and we want to handle all the clicks in the same place. Instead of creating onClickListener inside the method call, we will make the Fragment implement OnClikListener by setting onClickListener to this. The this keyword refers to the whole fragment here, so the fragment will be listening for the click in the onClick method, which is mandatory to implement once the Fragment implements View.OnClickListener.

The OnClick() method receives a view, which is the view clicked on. If we compare that view's ID with the ID of the button, we will know whether the button or the other view where we set clickListener has been clicked.

Just type implements View.OnClickListener when defining the class, and you will be asked to implement the mandatory methods:

/**
* Settings Fragment
*/
public class SettingsFragment extends Fragment implements View.OnClickListener {
  
  
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
  Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View view = inflater.inflate(R.layout.fragment_settings, container, false);
    
    view.findViewById(R.id.settingsButtonListExample).setOnClickListener(this);
    
    view.findViewById(R.id.ViewX).setOnClickListener(this);
    
    view.findViewById(R.id.imageY).setOnClickListener(this);
    
    
    return view;
  }
  
  @Override
  public void onClick(View view) {
    switch (view.getId()){
      case (R.id.settingsButtonListExample) :
      showDialog();
      break;
      case (R.id.viewX) :
      //Example
      break;
      case (R.id.imageY) :
      //Example
      break;
      
      //...
    }
  }
  
  public void showListDialog(){
    //Show Dialog here
  }
}

You will notice that we also move the logic to show the list dialog to an external method, keeping the structure easy to read in onClick();.

Continuing with the dialog, we can show an AlertDialog that has a setAdapter() property, which automatically binds the items with an internal ListView. Alternatively, we could create a view for our dialog with ListView on it and then set the adapter to that ListView:

/**
*  Show a dialog with different options to choose from
*/
public void showListDialog(){
  
  AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
  
  final ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(
  getActivity(),
  android.R.layout.select_dialog_singlechoice);
  arrayAdapter.add("Option 0");
  arrayAdapter.add("Option 1");
  arrayAdapter.add("Option 2");
  
  builder.setTitle("Choose an option");
  
  builder.setAdapter(arrayAdapter,
  new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialogInterface, int i) {
      Toast.makeText(getActivity(),"Option choosen "+i, Toast.LENGTH_SHORT).show();
      dialogInterface.dismiss();
    }
  });
  
  builder.show();
}

This dialog will show a message indicating the option clicked. We have used android.R.layout.select_dialog_singlechoice as a view for our rows.

These are a few different examples of built-in layouts for lists, which will depend on the theme of our application. The dialog won't look the same in 4.4 KitKat and in 5.0 Lollipop, for instance, in android.R.layout.simple_list_item_1, this is how it will look:

Using ListViews with built-in views

Here's what android.R.layout.simple_list_item_2 with two rows will look similar to:

Using ListViews with built-in views

This is an example of android.R.layout.simpleListItemChecked, where we can change the choice mode to multiple or single:

Using ListViews with built-in views

This is android.R.layout.activityListItem, where we have an icon and text:

Using ListViews with built-in views

We can access these built-in layout components to tweak the view a bit more when creating the layout. These components are named android.resource.id.Text1, android.resource.id.Text2, android.resource.id.Icon, and so on.

Now, we have an idea of how to create lists with the functionality and views ready to be used. It's time to create our own Adapter and implement the functionality and the view manually.

Creating a custom Adapter

When you look for a job, apart from looking at offers, you would also be handing your CV to different software companies or to IT recruitment companies that will find a company for you.

In our contact fragment, we will create a list sorted by country, displaying the contact details of these companies. There will be two different rows: one for the country header and another one for the company details.

We can create another table in our Parse database, called JobContact, with the following fields:

Creating a custom Adapter

We will request the job contacts from the server and build a list of items that will be sent to the Adapter to build the list. In the list, we will send two different elements: the company and the country. What we can do is generate a list of items and add the two as objects. Our two classes will look similar to the following:

@ParseClassName("JobContact")
public class JobContact extends ParseObject {
  
  public JobContact() {
    // A default constructor is required.
  }
  
  public String getName() {
    return getString("name");
  }
  
  public String getDescription() {
    return getString("description");
  }
  
  public String getCountry() {
    return getString("country");
  }
  
  public String getEmail() {
    return getString("email");
  }
  
}

public class Country {
  
  String countryCode;
  
  public Country(String countryCode) {
    this.countryCode = countryCode;
  }
  
}

Once we download the information sorted by country from http://www.parse.com, we can build our list of items, iterating through the parse list and adding a country header when a different country is detected. Execute the following code:

public void retrieveJobContacts(){
  ParseQuery<JobContact> query = ParseQuery.getQuery("JobContact");
  query.orderByAscending("country");
  query.findInBackground(new FindCallback<JobContact>() {
    @Override
    public void done(List<JobContact> jobContactsList, ParseException e) {
      mListItems = new ArrayList<Object>();
      String currentCountry = "";
      for (JobContact jobContact: jobContactsList) {
        if (!currentCountry.equals(jobContact.getCountry())){
          currentCountry = jobContact.getCountry();
          mListItems.add(new Country(currentCountry));
        }
        mListItems.add(jobContact);
      }
    }
  });
}

Now that we have our list with the headers included we are ready to create the Adapter based on this list, which will be sent as a parameter in the constructor. The best way to customize an Adapter is to create a subclass extending BaseAdapter. Once we do this, we will be asked to implement the following methods:

public class JobContactsAdapter extends BaseAdapter {
  @Override
  public int getCount() {
    return 0;
  }
  
  @Override
  public Object getItem(int i) {
    return null;
  }
  
  @Override
  public long getItemId(int i) {
    return 0;
  }
  
  @Override
  public View getView(int i, View view, ViewGroup viewGroup) {
    return null;
  }
}

These methods will have to be implemented according to the data that we want to display; for instance, getCount() will have to return the size of the list. We need to implement a constructor receiving two parameters: the list and the context. The context will be necessary to inflate the list in the getView() method. This is how the adapter will look without implementing getView():

public class JobContactsAdapter extends BaseAdapter {
  
  private List<Object> mItemsList;
  private Context mContext;
  
  public JobContactsAdapter(List<Object> list, Context context){
    mItemsList = list;
    mContext = context;
  }
  
  @Override
  public int getCount() {
    return mItemsList.size();
  }
  
  @Override
  public Object getItem(int i) {
    return mItemsList.get(i);
  }
  
  @Override
  public long getItemId(int i) {
    //Not needed
    return 0;
  }
  
  @Override
  public View getView(int i, View view, ViewGroup viewGroup) {
    return null;
  }
}

In our case, we can create two different views; so, apart from the mandatory methods, we need to implement two extra methods:

@Override
public int getItemViewType(int position) {
  return mItemsList.get(position) instanceof Country ? 0 : 1;
}

@Override
public int getViewTypeCount() {
  return 2;
}

The getItemViewType method will return 0 if the element is a country or 1 if the element is a company. With the help of this method, we can implement getView(). In case it's a country, we inflate row_job_country.xml, which contains ImageView and TextView; in case it's a company, we inflate row_job_contact.xml, which contains three text views:

@Override
public View getView(int i, View view, ViewGroup viewGroup) {
  
  View rowView = null;
  switch (getItemViewType(i)){
    
    case (0) :
    rowView = View.inflate(mContext, R.layout.row_job_country,null);
    Country country = (Country) mItemsList.get(i);
    ((TextView) rowView.findViewById(R.id.rowJobCountryTitle)).setText(country.getName());
    ((ImageView) rowView.findViewById(R.id.rowJobCountryImage)).setImageResource(country.getImageRes(mContext));
    break;
    
    case (1) :
    rowView = View.inflate(mContext, R.layout.row_job_contact,null);
    JobContact company = (JobContact) mItemsList.get(i);
    ((TextView) rowView.findViewById(R.id.rowJobContactName)).setText(company.getName());
    ((TextView) rowView.findViewById(R.id.rowJobContactEmail)).setText(company.getEmail());
    ((TextView) rowView.findViewById(R.id.rowJobContactDesc)).setText(company.getDescription());
  }
  
  return rowView;
}

To finish, we can create ListView in contact_fragment.xml and set the adapter to this list. However, we will take a shortcut and use android.support.v4.ListFragment; this is a fragment that already inflates a view with ListView and contains the setListAdapter() method, which sets an adapter to the built-in ListView. Extending from this fragment, our ContactFragment class will look similar to the following code:

public class ContactFragment extends android.support.v4.app.ListFragment {
  
  List<Object> mListItems;
  
  public ContactFragment() {
    // Required empty public constructor
  }
  
  @Override
  public void onViewCreated(View view, Bundle bundle) {
    super.onViewCreated(view,bundle);
    retrieveJobContacts();
  }
  
  public void retrieveJobContacts(){
    ParseQuery<JobContact> query = ParseQuery.getQuery("JobContact");
    query.orderByAscending("country");
    query.findInBackground(new FindCallback<JobContact>() {
      @Override
      public void done(List<JobContact> jobContactsList, ParseException e) {
        mListItems = new ArrayList<Object>();
        String currentCountry = "";
        for (JobContact jobContact: jobContactsList) {
          if (!currentCountry.equals(jobContact.getCountry())){
            currentCountry = jobContact.getCountry();
            mListItems.add(new Country(currentCountry));
          }
          mListItems.add(jobContact);
        }
        setListAdapter(new JobContactsAdapter(mListItems,getActivity()));
      }
    });
  }
}

Upon calling the retrieveJobContacts() method after the view has been created, we achieve the following result:

Creating a custom Adapter

The flags that we have displayed are images in the drawable folder whose name matches the country code, drawable/ "country_code" .png. We can display them by setting the resource identifier to ImageView and retrieving it with the following method inside the Country class:

public int getImageRes(Context ctx){
  return ctx.getResources().getIdentifier(countryCode, "drawable", ctx.getPackageName());
}

This is a basic version of ListView with two different types of rows. This version is still far from perfect; it lacks performance. It does not recycle the views, and it finds the IDs of the widget every time we create a row. We will explain and solve this problem in the following section.

Recycling views

While working with ListView, we need to keep in mind that the number of rows is a variable and we always want the list to feel fluent even if we scroll as quickly as we can. Hopefully, Android helps us a lot with this task.

When we scroll through ListView, the views that are not visible anymore on one side of the screen are reused and displayed again on the other side. This way, android saves inflation of the views; when it inflates, a view has to go through the xml nodes, instantiating every component. This extra computation can be the difference between a fluent and staggering list.

Recycling views

The getView() method receives as a parameter one of the views that are to be recycled or null if there are no views to be recycled.

To take advantage of this view recycling, we need to stop creating a view every time and reuse the view coming as a parameter. We still need to change the value of the text views and widget inside the row on a recycled view because it has the initial values that correspond to its previous position. In our example, we have an extra complication; we cannot recycle a country view to be used for a company view, so we can only recycle views of the same view type. However, again, Android does that check for us using internally the getItemViewType method that we implemented:

@Override
public View getView(int i, View view, ViewGroup viewGroup) {
  
  switch (getItemViewType(i)){
    
    case (0) :
    if (view == null){
      view = View.inflate(mContext, R.layout.row_job_country,null);
    }
    Country country = (Country) mItemsList.get(i);
    ((TextView) view.findViewById(R.id.rowJobCountryTitle)).setText(country.getName());
    ((ImageView) view.findViewById(R.id.rowJobCountryImage)).setImageResource(country.getImageRes(mContext));
    break;
    
    case (1) :
    if (view == null){
      view = View.inflate(mContext, R.layout.row_job_contact,null);
    }
    JobContact company = (JobContact) mItemsList.get(i);
    ((TextView) view.findViewById(R.id.rowJobContactName)).setText(company.getName());
    ((TextView) view.findViewById(R.id.rowJobContactEmail)).setText(company.getEmail());
    ((TextView) view.findViewById(R.id.rowJobContactDesc)).setText(company.getDescription());
  }
  
  return view;
}

Applying the ViewHolder pattern

Note that in getView(), every time we want to set a text to TextView, we search this TextView in row view with the findViewById() method; even when the row is recycled, we still find the TextView again to set the new value.

We can create a class called ViewHolder, which holds the reference to the widget by saving the computation of the widget search inside the row. This ViewHolder class will only contain references to the widgets, and we can keep a reference between a row and its ViewHolder class through the setTag() method. A View object allows us to set an object as a tag and retrieve it later; we can add as many tags as we want by specifying a key for this tag: setTag(key) or getTag(key). If no key is specified, we can save and retrieve the default tag.

Following this pattern for the first time that we create the view, we will create the ViewHolder class and set it as a tag to the view. If the view is already created and we are recycling it, we will simply retrieve the holder. Execute the following code:

@Override
public View getView(int i, View view, ViewGroup viewGroup) {
  
  switch (getItemViewType(i)){
    
    case (0) :
    CountryViewHolder holderC;
    if (view == null){
      view = View.inflate(mContext, R.layout.row_job_country,null);
      holderC = new CountryViewHolder();
      holderC.name = (TextView) view.findViewById(R.id.rowJobCountryTitle);
      holderC.flag = (ImageView) view.findViewById(R.id.rowJobCountryImage);
      view.setTag(view);
    } else {
      holderC = (CountryViewHolder) view.getTag();
    }
    Country country = (Country) mItemsList.get(i);
    holderC.name.setText(country.getName());
    holderC.flag.setImageResource(country.getImageRes(mContext));
    break;
    case (1) :
    CompanyViewHolder holder;
    if (view == null){
      view = View.inflate(mContext, R.layout.row_job_contact,null);
      holder = new CompanyViewHolder();
      holder.name = (TextView) view.findViewById(R.id.rowJobContactName);
      holder.email = (TextView) view.findViewById(R.id.rowJobContactEmail);
      holder.desc = (TextView) view.findViewById(R.id.rowJobOfferDesc);
      view.setTag(holder);
    } else {
      holder = (CompanyViewHolder) view.getTag();
    }
    JobContact company = (JobContact) mItemsList.get(i);
    holder.name.setText(company.getName());
    holder.email.setText(company.getEmail());
    holder.desc.setText(company.getDescription());
  }
  
  return view;
}

private class CountryViewHolder{
  
  public TextView name;
  public ImageView flag;
  
}

private class CompanyViewHolder{
  
  public TextView name;
  public TextView email;
  public TextView desc;
  
}

To simplify this code, we can create a method called bindView() inside each holder; it will get a country or company object and populate the widgets:

CountryViewHolder holderC;
if (view == null){
  view = View.inflate(mContext, R.layout.row_job_country,null);
  holderC = new CountryViewHolder(view);
  view.setTag(view);
} else {
  holderC = (CountryViewHolder) view.getTag();
}
holderC.bindView((Country)mItemsList.get(i));
break;



private class CountryViewHolder{
  
  public TextView name;
  public ImageView flag;
  
  public CountryViewHolder(View view) {
    this.name = (TextView) view.findViewById(R.id.rowJobCountryTitle);
    this.flag = (ImageView) view.findViewById(R.id.rowJobCountryImage);
  }
  
  public void bindView(Country country){
    this.name.setText(country.getName());
    this.flag.setImageResource(country.getImageRes(mContext));
  }
  
}

We will now finish with the list of ListView performance improvements. If there are images or long operations to load a view, we need to create AsyncTask method inside getView() so as to avoid heavy operation while scrolling. For instance, if we want to display an image downloaded from the Internet on every row, we would have a LoadImageAsyncTask method, which we will execute with the holder and the URL to download the image from. When the Asynctask method finishes, it will have a reference to the holder and will therefore be able to display the image:

public View getView(int position, View convertView,
ViewGroup parent) {
  
  
  ...
  
  
  new LoadImageAsyncTask(list.get(position).getImageUrl, holder)
  .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
  
  return convertView;
}

Now that we know all of the different techniques to improve the performance of a ListView, we are ready to introduce RecyclerView. By applying most of these techniques in the implementation, we will be able to identify it easily.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset