title: Android: What to do if performance of ListView is still not enough?
body: <p>Well this topic was and still is debated really a lot and I already read many tutorials, hints and saw talks about it. But I still have problems with my implementation of a custom BaseAdapter for a ListView whenever I reach a certain complexity of my rows.
  So what I basically have are some entities I'm getting by parsing xml coming from the network. In addition I fetch some Images, etc. and all this is done in an AsyncTask.
  I use the performance optimizing ViewHandler approach within my getView() method and reuse convertView as suggested by everyone. I.e. I hope that I'm using ListView as it's supposed to be and it really works fine when I'm just displaying a single ImageView and two TextViews, which are styled with a SpannableStringBuilder (I don't use any HTML.fromHTML whatsoever).</p>
  
  <p>And now here it comes. Whenever I extend my row layout with multiple small ImageViews, a Button and some more TextViews all differently styled with SpannableStringBuilder, I get a ceasing scroll performance. The row consists of a RelativeLayout as a parent and all other elements are arranged with layout parameters, so I can't get the row to be more simple in its layout. I must admit that I never saw any example of a ListView implementation with rows containing that many UI elements.</p>
  
  <p>However, when I'm using a TableLayout within a ScrollView and filling it by hand with an AsyncTask (new rows added steadily by onProgressUpdate() ), it behaves perfectly smooth even with hundreds of rows in it. It just stumbles a little bit when new rows are added if scrolled to the end of the list. Otherwise it's much smoother than with the ListView, where it's always stumbling when scrolled.</p>
  
  <p>Are there any suggestions what to do when a ListView just doesn't want to perform well? Should I stay with the TableLayout approach or is it advised to fiddle with a ListView to optimize the performance a bit?</p>
  
  <p>Here is the implementation of my adapter:</p>
  
  <pre><code>protected class BlogsSeparatorAdapter extends BaseAdapter {
  
          private LayoutInflater inflater;
          private final int SEPERATOR = 0;
          private final int BLOGELEMENT = 1;
  
          public BlogsSeparatorAdapter(Context context) {
                  inflater = LayoutInflater.from(context);
          }
  
          @Override
          public int getCount() {
                  return blogs.size();
          }
  
          @Override
          public Object getItem(int position) {
                  return position;
          }
  
          @Override
          public int getViewTypeCount() {
                  return 2;
          }
  
          @Override
          public int getItemViewType(int position) {
                  int type = BLOGELEMENT;
                  if (position == 0) {
                          type = SEPERATOR;
                  } else if (isSeparator(position)) {
                          type = SEPERATOR;
                  }
                  return type;
          }
  
          @Override
          public long getItemId(int position) {
                  return position;
          }
  
          @Override
          public View getView(int position, View convertView, ViewGroup parent) {
                  UIBlog blog = getItem(position);
              ViewHolder holder;
              if (convertView == null) {
              holder = new ViewHolder();
  
              convertView = inflater.inflate(R.layout.blogs_row_layout, null);
              holder.usericon = (ImageView) convertView.findViewById(R.id.blogs_row_user_icon);
              holder.title = (TextView) convertView.findViewById(R.id.blogs_row_title);
              holder.date = (TextView) convertView.findViewById(R.id.blogs_row_date);
              holder.amount = (TextView) convertView.findViewById(R.id.blogs_row_cmmts_amount);
              holder.author = (TextView) convertView.findViewById(R.id.blogs_row_author);
              convertView.setTag(holder);
          } else {
              holder = (ViewHolder) convertView.getTag();
          }           
          holder.usericon.setImageBitmap(blog.icon);
          holder.title.setText(blog.titleTxt);
          holder.date.setText(blog.dateTxt);
          holder.amount.setText(blog.amountTxt);
          holder.author.setText(blog.authorTxt);          
  
                      return convertView;
          }
  
          class ViewHolder {
                  TextView separator;
                  ImageView usericon;
                  TextView title;
                  TextView date;
                  TextView amount;
                  TextView author;
          }
  
          /**
           * Check if the blog on the given position must be separated from the last blogs.
           * 
           * @param position
           * @return
           */
          private boolean isSeparator(int position) {
                  boolean separator = false;
                  // check if the last blog was created on the same date as the current blog
                  if (DateUtility.getDay(
                                  DateUtility.createCalendarFromUnixtime(blogs.get(position - 1).getUnixtime() * 1000L), 0)
                                  .getTimeInMillis() &gt; blogs.get(position).getUnixtime() * 1000L) {
                          // current blog was not created on the same date as the last blog --&gt; separator necessary
                          separator = true;
                  }
                  return separator;
          }
  }
  </code></pre>
  
  <p>This is the xml for the row (no button, still stumbling):</p>
  
  <pre><code>&lt;RelativeLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="wrap_content"
      android:layout_height="fill_parent"
      android:background="@drawable/listview_selector"&gt;
      &lt;ImageView
          android:id="@+id/blogs_row_user_icon"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_alignParentTop="true"
          android:layout_alignParentLeft="true"
          android:paddingTop="@dimen/blogs_row_icon_padding_top"
          android:paddingLeft="@dimen/blogs_row_icon_padding_left"/&gt;
      &lt;TextView
          android:id="@+id/blogs_row_title"
          android:layout_toRightOf="@id/blogs_row_user_icon"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:padding="@dimen/blogs_row_title_padding"
          android:textColor="@color/blogs_table_text_title"/&gt;
      &lt;TextView
          android:id="@+id/blogs_row_date"
          android:layout_below="@id/blogs_row_title"
          android:layout_toRightOf="@id/blogs_row_user_icon"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:paddingLeft="@dimen/blogs_row_date_padding_left"
          android:textColor="@color/blogs_table_text_date"/&gt;
      &lt;ImageView
          android:id="@+id/blogs_row_cmmts_icon"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_below="@id/blogs_row_title"
          android:layout_toRightOf="@id/blogs_row_date"
          android:layout_margin="@dimen/blogs_row_cmmts_icon_margin"
          android:src="@drawable/comments"/&gt;
      &lt;TextView
          android:id="@+id/blogs_row_cmmts_amount"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_below="@id/blogs_row_title"
          android:layout_toRightOf="@id/blogs_row_cmmts_icon"
          android:layout_margin="@dimen/blogs_row_author_margin"/&gt;
      &lt;TextView
          android:id="@+id/blogs_row_author"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_below="@id/blogs_row_title"
          android:layout_toRightOf="@id/blogs_row_cmmts_amount"
          android:marqueeRepeatLimit="marquee_forever"
          android:singleLine="true"
          android:ellipsize="marquee"
          android:layout_margin="@dimen/blogs_row_author_margin"/&gt;
  &lt;/RelativeLayout&gt;
  </code></pre>
  
  <p><em><strong></em>**<em>*</em>**<em>*</em>**<em>*</em>*</strong> UPDATE <strong><em>*</em>**<em>*</em>**<em>*</em>**<em>*</em>**<em>*</em></strong></p>
  
  <p>As it turned out the problem was simply solved by using ArrayAdapter instead of a BaseAdapter. I used the exact same code with an ArrayAdapter and the performance difference is GIGANTIC! It runs just as smooth as with a TableLayout.</p>
  
  <p>So whenever I'm using ListView, I will definitely avoid using BaseAdapter as it is significantly slower and less optimized for complex layouts. This is a rather interesting conclusion because I hadn't read a word about it in examples and tutorials. Or perhaps I wasn't reading it accurately. ;-)</p>
  
  <p>Well however this is the code that is working smoothly (as you can see my solution is using seperators to group the list):</p>
  
  <pre><code>protected class BlogsSeparatorAdapter extends ArrayAdapter&lt;UIBlog&gt; {
  
      private LayoutInflater inflater;
  
      private final int SEPERATOR = 0;
      private final int BLOGELEMENT = 1;
  
      public BlogsSeparatorAdapter(Context context, List&lt;UIBlog&gt; rows) {
          super(context, R.layout.blogs_row_layout, rows);
          inflater = LayoutInflater.from(context);
      }
  
      @Override
      public int getViewTypeCount() {
          return 2;
      }
  
      @Override
      public int getItemViewType(int position) {
          int type = BLOGELEMENT;
          if (position == 0) {
              type = SEPERATOR;
          } else if (isSeparator(position)) {
              type = SEPERATOR;
          }
          return type;
      }
  
      @Override
      public long getItemId(int position) {
          return position;
      }
  
      @Override
      public View getView(int position, View convertView, ViewGroup parent) {
          final UIBlog blog = uiblogs.get(position);
          int type = getItemViewType(position);
  
          ViewHolder holder;
          if (convertView == null) {
              holder = new ViewHolder();
              if (type == SEPERATOR) {
                  convertView = inflater.inflate(R.layout.blogs_row_day_separator_item_layout, null);
                  View separator = convertView.findViewById(R.id.blogs_separator);
                  separator.setOnClickListener(new OnClickListener() {
                      @Override
                      public void onClick(View v) {
                          // do nothing
                      }
                  });
                  holder.separator = (TextView) separator.findViewById(R.id.blogs_row_day_separator_text);
              } else {
                  convertView = inflater.inflate(R.layout.blogs_row_layout, null);
              }
              holder.usericon = (ImageView) convertView.findViewById(R.id.blogs_row_user_icon);
              holder.title = (TextView) convertView.findViewById(R.id.blogs_row_title);
              holder.date = (TextView) convertView.findViewById(R.id.blogs_row_date);
              holder.amount = (TextView) convertView.findViewById(R.id.blogs_row_author);
              holder.author = (TextView) convertView.findViewById(R.id.blogs_row_author);
              convertView.setTag(holder);
          } else {
              holder = (ViewHolder) convertView.getTag();
          }
          if (holder.separator != null) {
              holder.separator
                      .setText(DateUtility.createDate(blog.blog.getUnixtime() * 1000L, "EEEE, dd. MMMMM yyyy"));
          }
          holder.usericon.setImageBitmap(blog.icon);
          holder.title.setText(createTitle(blog.blog.getTitle()));
          holder.date.setText(DateUtility.createDate(blog.blog.getUnixtime() * 1000L, "'um' HH:mm'Uhr'"));
          holder.amount.setText(createCommentsAmount(blog.blog.getComments()));
          holder.author.setText(createAuthor(blog.blog.getAuthor()));
          return convertView;
      }
  
      class ViewHolder {
          TextView separator;
          ImageView usericon;
          TextView title;
          TextView date;
          TextView amount;
          TextView author;
      }
  
      /**
       * Check if the blog on the given position must be separated from the last blogs.
       * 
       * @param position
       * @return
       */
      private boolean isSeparator(int position) {
          boolean separator = false;
          // check if the last blog was created on the same date as the current blog
          if (DateUtility.getDay(
                  DateUtility.createCalendarFromUnixtime(blogs.get(position - 1).getUnixtime() * 1000L), 0)
                  .getTimeInMillis() &gt; blogs.get(position).getUnixtime() * 1000L) {
              // current blog was not created on the same date as the last blog --&gt; separator necessary
              separator = true;
          }
          return separator;
      }
  }
  </code></pre>
  
  <p><strong>+++++++++++++++++ SECOND EDIT WITH TRACES +++++++++++++++++++++</strong>
  Just to show that BaseAdapter DOES something different than the ArrayAdapter. This is just the whole trace coming from the getView() method with the EXACT same code in both adapters.</p>
  
  <p>First the amount of calls <a href="http://img845.imageshack.us/img845/5463/tracearrayadaptercalls.png">http://img845.imageshack.us/img845/5463/tracearrayadaptercalls.png</a> </p>
  
  <p><a href="http://img847.imageshack.us/img847/7955/tracebaseadaptercalls.png">http://img847.imageshack.us/img847/7955/tracebaseadaptercalls.png</a></p>
  
  <p>Exclusive time consumption
  <a href="http://img823.imageshack.us/img823/6541/tracearrayadapterexclus.png">http://img823.imageshack.us/img823/6541/tracearrayadapterexclus.png</a></p>
  
  <p><a href="http://img695.imageshack.us/img695/3613/tracebaseadapterexclusi.png">http://img695.imageshack.us/img695/3613/tracebaseadapterexclusi.png</a></p>
  
  <p>Inclusive time consumption
  <a href="http://img13.imageshack.us/img13/4403/tracearrayadapterinclus.png">http://img13.imageshack.us/img13/4403/tracearrayadapterinclus.png</a></p>
  
  <p><a href="http://img831.imageshack.us/img831/1383/tracebaseadapterinclusi.png">http://img831.imageshack.us/img831/1383/tracebaseadapterinclusi.png</a></p>
  
  <p>As you can see there is a HUGE difference (ArrayAdapter is four times faster in the getView() method) between those two adapters. And I really don't have any idea why this is so dramatic. I can only assume that ArrayAdapter has some sort of better caching or further optimizations.</p>
  
  <p><strong>++++++++++++++++++++++++++JUST ANOTHER UPDATE+++++++++++++++++</strong>
  To show you how my current UIBlog class is built:</p>
  
  <pre><code>private class UIBlog {
      Blog blog;
      CharSequence seperatorTxt;
      Bitmap icon;
      CharSequence titleTxt;
      CharSequence dateTxt;
      CharSequence amountTxt;
      CharSequence authorTxt;
  }
  </code></pre>
  
  <p>Just to make it clear, I'm using this for BOTH adapters.</p>