Published

Fri 12 December 2014

←Home

Android optimizing/tuning ListView

ListView is probably the most common android component, but it has to be implemented correctly to provide a  better user experience.

In this post, I will give a few suggestions on how you can achieve the near optimum list performance.  They are mentioned below:

  1. Access Data Incrementally:- Load only the data you need. I know this is a general principle to reduce memory footprint of your application, but even so, this can and must be done.  The ListView class can listen for scroll updates, you can implement the OnScrollListener interface to do so.  When the user scrolls to the bottom of the list, then load the incremental data. This can be done independent of the data source, whether it be a REST web service or the SQLite database. In this post, I am going to show you how you can handle this when your view is backed by the data from the database. Take a look at this abstract class Endless Scroll Listener which implements the OnScrollListener. To integrate this with your data source and ListView all you  have to do is extend from this class as shown below.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // The listener implementation
    listener= new EndlessScrollListener() {
              @Override
              public void onLoadMore(int page, int totalItemsCount) {
                  // Triggered only when new data needs to be appended to the list
                  // Add whatever code is needed to append new items to your AdapterView
                  customLoadMoreDataFromApi(page);
                  // or customLoadMoreDataFromApi(totalItemsCount);
              }
    private void customLoadMoreDataFromApi(int page) {
                  //Progress dialog to show with loading text
                  pd.setTitle(R.string.loadingText);
                  pd.show();
                  Bundle arg = new Bundle();
                 //here pageSize is a constant
               //How many records to fetch
                  limit =  pageSize;
                 //From which offset
                  offset = (page - 1) * pageSize;
               //add these to the parameters that will be passed to the loader
                  arg.putInt("limit", limit);
                  arg.putInt("offset", offset);
                 //add custom arguments
    
                //Instantiate or restart the loader
                   Loader loader = getActivity().getSupportLoaderManager().getLoader(LOADER_ID);
                  if (loader != null && !loader.isReset()) {
                      getActivity().getSupportLoaderManager().restartLoader(LOADER_ID, arg, _self);
                  } else {
                      getActivity().getSupportLoaderManager().initLoader(LOADER_ID, arg, _self);
                  }
              }
                };
              //set on scroll listener for the listview
              lstView.setOnScrollListener(listener);
    

SQLite provides limit and offset clause, so when the user scrolls downwards, we restart the loader with the new offset parameter. Below I have mentioned the methods involved in loader callback implementations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
  public Loader<Object> onCreateLoader(int id, Bundle args) {
    Loader loader= new SimpleAbstractListLoader(getActivity().getApplicationContext(),id,args);
    return loader;
    }
 @Override
 public void onLoadFinished(Loader<Object> arg0,
         Object arg1) {
             //here article list is an array list that contains the data
           //if it is initial just create a new list else append the data to the existing list
           if(articleList==null||adapter==null){
             //FeedArticleSummary is a POJO class
                    articleList=new ArrayList<FeedArticleSummary>();
                    articleList.addAll((ArrayList<FeedArticleSummary>) arg1);
              //Custom Adapter for the listview that extends the ArrayAdapter
                    adapter = new ArticleListAdapter(this.getActivity(),
                            R.layout.article_list_layout,articleList);
                    lstView.setAdapter(adapter);
                }
                else{
                    articleList.addAll((ArrayList<FeedArticleSummary>)arg1);
                    adapter.notifyDataSetChanged();


                }
              //load finished so dismiss the progress dialog.
                pd.dismiss();
            }

Now the loader class SimpleAbstractListLoader just needs to pass the limit and offset parameters to a database utility class which will apply limit and offset to the SQL Query to fetch the data. i.e rawQuery+whereClause+OrderBy + limitandOffsetClause. Here one thing should be noted that order by clause is not optional and is required as the data needs to be fetched in a predefined order so that the offset clause can fetch the correct data.

Use a ViewHolder and know that views can be recycled:  A ViewHolder pattern avoids repeated calls to findViewById() which is used to lookup the views in layout.  It is just a static class whose instance holds the component views inside the tag field of the Layout.  Secondly, List Views can be recycled, so its a best practice to reuse the views and populate them with new data, instead of inflating them  again.  These two when combined together will make the scrolling of the list very smooth and fast.  Here the latter concept is even more important than the former, as inflating a view is a costly operation and should definitely be avoided.  So, if you are using ArrayAdapter as a data source for your list, this is how your getView() method should look like.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
    public View getView(int position, View convertView, ViewGroup parent) {
         ViewHolder holder;
       // Get the data item for this position
         //custom pojo which hold the reference to the data
      FeedArticleSummary articleSummary = getItem(position);
         if(articleSummary==null ){
             return null;
         }
        // Check if an existing view is being reused, otherwise inflate the view
      if (convertView == null) {
        convertView = mInflater.inflate(R.layout.article_list_layout, null);
           holder=new ViewHolder();
           holder.text=(TextView) convertView.findViewById(R.id.textViewArticleListFragment);
           holder.imageButton=(ImageButton)convertView.findViewById(R.id.favoriteImageArticleList);
           holder.checkBox= (CheckBox) convertView.findViewById(R.id.selected);
         //Here the holder is set as a tag field in the layout
           convertView.setTag(holder);
        }
         else{
            holder=(ViewHolder)convertView.getTag();
        }
        //set the data here
        //for example
      // holder.checkBox.setChecked(true);
       ....
       }

And here is the ViewHolder class.

1
2
3
4
5
6
7
       static class ViewHolder {
            TextView text;
            CheckBox checkBox;
            ImageButton imageButton;
        }

|

Do not set the layout_height and layout_width property to wrap_content:  Definitely avoid this because getView() will be invoked a number of times,  to determine the height and width for the views that are to be drawn. Instead use fill_parent or use fixed height and width, so that there are no unnecessary invocations of the getView() method.

If you know which row has been modified call getView() instead of notifyDataSetChanged().  Often, a user might modify a single item in  a list and you might be required to make some UI modification for that item. For example, if a user selects a list item to view the details, you might change the font style of that particular listitem, to show that the item has been read. In this case it is much faster to just invoke the getView() for that item rather than calling notifyDataSetChanged().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
 public void onListFragmentItemClick(ListView l, View view, int position,
          long id) {

      ListView lstView =  l;
        //adapter reference
       ArticleListAdapter articleAdapter = (ArticleListAdapter) l
                .getAdapter();
      //accessor for the underlying list
        ArrayList<FeedArticleSummary> articleSummaries=articleAdapter.getArticleSummaries();
      FeedArticleSummary articleSummary = articleAdapter.getItem(position);
        if(articleSummary.getArticleRead()==null||articleSummary.getArticleRead().equals("N")) {
        //update the underlying data source for the list item
      articleSummary.setArticleRead("Y");
     int visiblePosition = lstView.getFirstVisiblePosition();
        View selectedChildView = lstView.getChildAt(position - visiblePosition);
        //update the data in the DB using a loader
         SQLiteCursorLoader loader = new SQLiteCursorLoader(this, fdb.getDbHelper(), null,
                    null);
            ContentValues cValues = new ContentValues();
            cValues.put(FeedSQLLiteHelper.COLUMN_ARTICLE_READ, "Y");
            loader.update(FeedSQLLiteHelper.TABLE_ARTICLES, cValues, "_id = ? ", new String[]{articleSummary.getArticleId()});
        //call getview
         lstView.getAdapter().getView(position, selectedChildView, lstView);
       ...
       }

and in your getView method handle the font style change

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    ...
    if(articleSummary.getArticleRead()==null|| articleSummary.getArticleRead().equals("N")){
                    holder.text.setTypeface(holder.text.getTypeface(),Typeface.BOLD);
                }
                else{
                    holder.text.setTypeface(holder.text.getTypeface(),Typeface.ITALIC);
                }
    ...

|

Note: Android has introduced RecyclerView, which forces you to use the ViewHolder as a best practice, this was not enforced earlier in ListView.

I might have missed on few other optimizations, so kindly do provide your feedback.

Go Top
comments powered by Disqus