Search code examples
androidlistviewandroid-listviewandroid-arrayadapterlayout-inflater

Android ListView - Called too many times OR changes order.. How to prevent both?


From the Android development page we have the following quote for Custom ArrayAdapter of ListViews:

"there is absolutely no guarantee on the order in which getView() will be called nor how many times."

When I have the following and getView is called a few times, it's no problem, since I'm just recycling the Views instead of inflating them again:

@Override
public void getView(int position, View convertView, ViewGroup parent){
    Log.i(TAG, "getView of position " + String.valueOf(position) + " is called");

    View view = convertView;
    if(view == null){
        view = inflater.inflate(layoutResourceId, parent, false);
        TextView myTextView1 = (TextView) view.findViewById(R.id.my_tv1);
        Button myButton1 = (Button) view.findViewById(R.id.my_btn1);
        ... // More views
        view.setTag(new MyViewHolder(myTextView1, myButton1, ...));
    }
    MyViewHolder h = (MyViewHolder) view.getTag();

    h.myTextView1.setText("Some text");
    h.myButton1.setText("Some button text");
    h.myButton1.setEnabled(buttonEnabled);
}

The problem with using the if(view == null) -> inflate view and create ViewHolder; else -> use ViewHolder of Tag is when you scroll up and down real fast in the list, the order of the items in the list changes. Could be because of the first part of the quote:

"there is absolutely no guarantee on the order in which getView() will be called..."

Though I think it's just a bug with ListViews itself instead of the Adapter. (Not sure, but whatever the cause is, it IS a BUG.)

The only way I was able to fix that so far is always inflating the view:

@Override
public void getView(int position, View convertView, ViewGroup parent){
    Log.i(TAG, "getView of position " + String.valueOf(position) + " is called");

    // NOTE: In Android it's recommended to use the convertView as the view, instead of
    // retrieving it every time from the inflater. But since there is a bug in Android ListViews
    // which changes the order of the Items when you scroll up- and down real fast,
    // I use the inflater every time and ignore the convertView parameter
    view = inflater.inflate(layoutResourceId, parent, false);
    TextView myTextView1 = (TextView) view.findViewById(R.id.my_tv1);
    Button myButton1 = (Button) view.findViewById(R.id.my_btn1);
    ... // More views
    view.setTag(new MyViewHolder(myTextView1, myButton1, ...));

    MyViewHolder h = (MyViewHolder) view.getTag();

    h.myTextView1.setText("Some text");
    h.myButton1.setText("Some button text");
    h.myButton1.setEnabled(buttonEnabled);
}

All works fine, the order of the items remain when scrolling up- and down fast. But now the performance is going down quickly.. For quite a long time I just ignored this performance issues, but right now I'm facing a new problem after I made some changes to the layout of my item. Never had this issue before, but now I face (for some unknown reason) the second part of the quote:

"there is absolutely no guarantee how many times getView() will be called..."

Right now with a ListView containing 14 visible elements at creation. When I go to the ChecklistActivity which contains the ListView with this Adapter, I get the following Logcat-messages:

07-23 03:17:14.516: V/MyAdapter(1395): getView(0)
07-23 03:17:16.556: V/MyAdapter(1395): getView(1)
07-23 03:17:16.946: V/MyAdapter(1395): getView(2)
07-23 03:17:17.316: V/MyAdapter(1395): getView(3)
07-23 03:17:17.776: V/MyAdapter(1395): getView(4)
07-23 03:17:18.136: V/MyAdapter(1395): getView(5)
07-23 03:17:18.286: V/MyAdapter(1395): getView(6)
07-23 03:17:18.466: V/MyAdapter(1395): getView(7)
07-23 03:17:18.576: V/MyAdapter(1395): getView(8)
07-23 03:17:18.806: V/MyAdapter(1395): getView(9)
07-23 03:17:19.016: V/MyAdapter(1395): getView(10)
07-23 03:17:19.246: V/MyAdapter(1395): getView(11)
07-23 03:17:19.346: D/dalvikvm(1395): GC_FOR_ALLOC freed 220K, 10% free 5545K/6108K, paused 82ms, total 85ms
07-23 03:17:19.526: V/MyAdapter(1395): getView(12)
07-23 03:17:19.666: V/MyAdapter(1395): getView(13)
07-23 03:17:19.786: V/MyAdapter(1395): getView(14)
07-23 03:17:20.046: I/Choreographer(1395): Skipped 1491 frames!  The application may be doing too much work on its main thread.
07-23 03:17:20.066: V/ChecklistActivity(1395): onCreateOptionsMenu
07-23 03:17:20.406: I/Choreographer(1395): Skipped 64 frames!  The application may be doing too much work on its main thread.
07-23 03:17:20.416: V/MyAdapter(1395): getView(2)
07-23 03:17:20.576: V/MyAdapter(1395): getView(3)
07-23 03:17:20.866: V/MyAdapter(1395): getView(4)
07-23 03:17:21.036: V/MyAdapter(1395): getView(5)
07-23 03:17:21.206: V/MyAdapter(1395): getView(6)
07-23 03:17:21.396: V/MyAdapter(1395): getView(7)
07-23 03:17:21.606: V/MyAdapter(1395): getView(8)
07-23 03:17:21.866: V/MyAdapter(1395): getView(9)
07-23 03:17:22.116: V/MyAdapter(1395): getView(10)
07-23 03:17:22.336: V/MyAdapter(1395): getView(11)
07-23 03:17:22.496: V/MyAdapter(1395): getView(12)
07-23 03:17:22.646: V/MyAdapter(1395): getView(13)
07-23 03:17:22.806: V/MyAdapter(1395): getView(14)
07-23 03:17:23.616: V/MainActivity(1395): onStop
07-23 03:17:23.616: I/Choreographer(1395): Skipped 160 frames!  The application may be doing too much work on its main thread.
07-23 03:17:24.246: I/Choreographer(1395): Skipped 112 frames!  The application may be doing too much work on its main thread.
07-23 03:17:24.286: V/MyAdapter(1395): getView(4)
07-23 03:17:24.396: D/dalvikvm(1395): GC_FOR_ALLOC freed 397K, 8% free 6099K/6568K, paused 100ms, total 107ms
07-23 03:17:24.646: V/MyAdapter(1395): getView(5)
07-23 03:17:24.756: V/MyAdapter(1395): getView(6)
07-23 03:17:24.886: V/MyAdapter(1395): getView(7)
07-23 03:17:25.046: V/MyAdapter(1395): getView(8)
07-23 03:17:25.196: V/MyAdapter(1395): getView(9)
07-23 03:17:25.356: V/MyAdapter(1395): getView(10)
07-23 03:17:25.486: V/MyAdapter(1395): getView(11)
07-23 03:17:25.596: V/MyAdapter(1395): getView(12)
07-23 03:17:25.726: V/MyAdapter(1395): getView(13)
07-23 03:17:25.846: V/MyAdapter(1395): getView(14)
07-23 03:17:26.146: I/Choreographer(1395): Skipped 35 frames!  The application may be doing too much work on its main thread.
07-23 03:17:26.406: V/MyAdapter(1395): getView(6)
07-23 03:17:26.586: V/MyAdapter(1395): getView(7)
07-23 03:17:26.786: V/MyAdapter(1395): getView(8)
07-23 03:17:26.896: V/MyAdapter(1395): getView(9)
07-23 03:17:27.156: V/MyAdapter(1395): getView(10)
07-23 03:17:27.296: V/MyAdapter(1395): getView(11)
07-23 03:17:27.436: V/MyAdapter(1395): getView(12)
07-23 03:17:27.646: V/MyAdapter(1395): getView(13)
07-23 03:17:27.766: V/MyAdapter(1395): getView(14)
07-23 03:17:28.186: I/Choreographer(1395): Skipped 56 frames!  The application may be doing too much work on its main thread.
07-23 03:17:28.426: D/dalvikvm(1395): GC_FOR_ALLOC freed 523K, 9% free 6713K/7308K, paused 108ms, total 112ms
07-23 03:17:28.506: I/Choreographer(1395): Skipped 60 frames!  The application may be doing too much work on its main thread.
07-23 03:17:28.606: V/MyAdapter(1395): getView(8)
07-23 03:17:28.806: V/MyAdapter(1395): getView(9)
07-23 03:17:28.926: V/MyAdapter(1395): getView(10)
07-23 03:17:29.136: V/MyAdapter(1395): getView(11)
07-23 03:17:29.256: V/MyAdapter(1395): getView(12)
07-23 03:17:29.406: V/MyAdapter(1395): getView(13)
07-23 03:17:29.516: V/MyAdapter(1395): getView(14)
07-23 03:17:29.816: I/Choreographer(1395): Skipped 35 frames!  The application may be doing too much work on its main thread.
07-23 03:17:30.096: V/MyAdapter(1395): getView(9)
07-23 03:17:30.216: V/MyAdapter(1395): getView(10)
07-23 03:17:30.366: V/MyAdapter(1395): getView(11)
07-23 03:17:30.496: V/MyAdapter(1395): getView(12)
07-23 03:17:30.646: V/MyAdapter(1395): getView(13)
07-23 03:17:30.796: V/MyAdapter(1395): getView(14)
07-23 03:17:31.166: I/Choreographer(1395): Skipped 53 frames!  The application may be doing too much work on its main thread.
07-23 03:17:31.376: I/Choreographer(1395): Skipped 32 frames!  The application may be doing too much work on its main thread.
07-23 03:17:31.426: V/MyAdapter(1395): getView(10)
07-23 03:17:31.546: V/MyAdapter(1395): getView(11)
07-23 03:17:31.736: V/MyAdapter(1395): getView(12)
07-23 03:17:31.906: V/MyAdapter(1395): getView(13)
07-23 03:17:32.046: V/MyAdapter(1395): getView(14)
07-23 03:17:32.306: I/Choreographer(1395): Skipped 31 frames!  The application may be doing too much work on its main thread.
07-23 03:17:32.536: I/Choreographer(1395): Skipped 35 frames!  The application may be doing too much work on its main thread.
07-23 03:17:32.606: V/MyAdapter(1395): getView(11)
07-23 03:17:32.816: V/MyAdapter(1395): getView(12)
07-23 03:17:32.986: D/dalvikvm(1395): GC_FOR_ALLOC freed 729K, 10% free 7323K/8124K, paused 121ms, total 128ms
07-23 03:17:33.166: V/MyAdapter(1395): getView(13)
07-23 03:17:33.496: V/MyAdapter(1395): getView(14)
07-23 03:17:34.056: V/MyAdapter(1395): getView(12)
07-23 03:17:34.186: V/MyAdapter(1395): getView(13)
07-23 03:17:34.316: V/MyAdapter(1395): getView(14)
07-23 03:17:34.596: I/Choreographer(1395): Skipped 34 frames!  The application may be doing too much work on its main thread.
07-23 03:17:34.846: I/Choreographer(1395): Skipped 36 frames!  The application may be doing too much work on its main thread.
07-23 03:17:34.906: V/MyAdapter(1395): getView(13)
07-23 03:17:35.016: V/MyAdapter(1395): getView(14)
07-23 03:17:35.546: V/MyAdapter(1395): getView(14)
07-23 03:17:36.066: I/Choreographer(1395): Skipped 31 frames!  The application may be doing too much work on its main thread.

I have absolutely no idea why it's called so many times, but I'd like it to only be called once each. And if someone knows a solution for this fast-scrolling bugs that changes the order without inflating the View every single time in the getView() it would also be a huge help.

PS: Ignore the first 07-23 03:17:20.046: I/Choreographer(1395): Skipped 1491 frames! The application may be doing too much work on its main thread. I'm currently working on that.. Inflating the View is one of the reasons why the performance lacks, but there are also some other issues that I'm trying to fix.

Still, why can't Android just have a robust ArrayAdapter. One that doesn't change the order of items when someone is scrolling up- and down fast. Or an options so you can choose to remember the items outside of the visible region.. This could theoretically also create performance issues if you remember to many items, but it will solve a lot of problems, including those with EditText-input being reset again after they are out of the visible region. Then we shouldn't have to use TextWatchers, Tags with Holders and such..

Ok, I'm sidetracking now.. It's just the way it is and I can't change that. So let's just focus on fixing the problems explained above (fast-scrolling bug without inflating every time AND getView not getting called so many times on creation).


EDIT 1:

Some more info about other parts of my code:

checklist_activity.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:descendantFocusability="afterDescendants" />

</RelativeLayout>

list_item.xml outer layout:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
<!-- height is wrap content since I have views that are Visible / Gone based on the Item's state -->

    <!-- All my View elements -->

</RelativeLayout>

For my item, see this different stackoverflow question of mine.


EDIT 2:

Here below a copy of my current MyAdapter.java class (PS: Removed comments, imports and Logs for easier readability). When I scroll up- and down fast in the Emulator, the Images stay the same, but the Texts (of the Product-Names, Prices and such) are being mixed up. I've got a list ordered by Category and Product-Name and I want to list to retain this order in the ListView.

public class MyAdapter extends ArrayAdapter<OrderedProductItem>
{
    private static final String TAG = "MyAdapter";
    public static MyAdapter mAdapter;

    private Context context;
    private int layoutResourceId;
    private LayoutInflater inflater;

    public MyAdapter(Context c, int layoutId, ArrayList<OrderedProductItem> objects){
        super(c, layoutId, objects);

        layoutResourceId = layoutId;
        context = c;
        inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        mAdapter = this;
    }

    private OnTouchListener itemOnTouchListener = new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if(v instanceof EditText){
                EditText et = (EditText)v;
                et.setFocusable(true);
                et.setFocusableInTouchMode(true);
            }
            else{
                ListItemTagHolder h = (ListItemTagHolder) v.getTag();
                h.etAmount.setFocusable(false);
                h.etAmount.setFocusableInTouchMode(false);
                h.actvName.setFocusable(false);
                h.actvName.setFocusableInTouchMode(false);
            }
            return false;
        }
    };

    @Override
    public View getView(int position, View convertView, ViewGroup parent){
        View view = convertView;
        ListItemTagHolder h;
        if(view == null){
            view = inflater.inflate(layoutResourceId, parent, false);
            RelativeLayout rl = (RelativeLayout) view.findViewById(R.id.item_layout);
            h = new ListItemTagHolder(rl);
            view.setTag(h);
        }
        else
            h = (ListItemTagHolder) view.getTag();

        /*
        View view = inflater.inflate(layoutResourceId, parent, false);
        RelativeLayout rl = (RelativeLayout) view.findViewById(R.id.item_layout);
        view.setTag(new ListItemTagHolder(rl));
        final ListItemTagHolder h = (ListItemTagHolder) view.getTag();
        */

        if(h != null){
            h.orderedProductItem = Controller.getInstance().getOrderedProductItems().get(position);

            MainActivity.getImageLoader().imageForImageView(h.orderedProductItem.getCheckState().rID(), h.imageView);

            String productName = Controller.getInstance().getProductById(h.orderedProductItem.getProductId()).getName();
            // (I use a Validation-class to compare Strings or Check if they aren't null/empty)
            if(!V.compare(h.tvName.getText().toString(), productName, true, true))
                h.tvName.setText(productName);

            String priceString = C.doubleToPrice(Controller.getInstance().getProductById(h.orderedProductItem.getProductId()).getPrice(), "€", true);
            if(!V.compare(h.tvPrice.getText().toString(), priceString, true, true))
                h.tvPrice.setText(priceString);

            String resultAmount = String.valueOf(h.orderedProductItem.getResultAmount());
            if(!V.compare(h.etAmount.getText().toString(), resultAmount, true, true))
                h.etAmount.setText(resultAmount);

            ChecklistActivity.cActivity.updateAutoCompleteTextViewWithFilter(h, h.orderedProductItem.getSelectedFilter());

            ChecklistActivity.cActivity.updateTagsSpinner(h, null);

            h.etAmount.addTextChangedListener(new MyTextWatcher(h.etAmount));
            h.actvName.addTextChangedListener(new MyTextWatcher(h.actvName));

            h.etAmount.setTag(h.orderedProductItem);
            h.actvName.setTag(h.orderedProductItem);

            ChecklistActivity.cActivity.updateChangeEditTexts(view, h);

            view.setOnTouchListener(itemOnTouchListener);
            h.etAmount.setOnTouchListener(itemOnTouchListener);
            h.actvName.setOnTouchListener(itemOnTouchListener);
        }

        return view;
    }
}

With the following Holder-class:

public class ListItemTagHolder
{
    public final RelativeLayout relativeLayout;

    public final LinearLayout leftLL, rightLL;
    public final ImageView imageView;
    public final TextView tvName, tvPrice, tvTags;
    public final EditText etAmount;
    public final AutoCompleteTextView actvName;
    public final Spinner spTags;
    public final ImageButton btnTags;
    public final Space spaceImage, spacePrice;
    public OrderedProductItem orderedProductItem;

    public ListItemTagHolder(RelativeLayout layout){
        relativeLayout = layout;

        leftLL = (LinearLayout) relativeLayout.findViewById(R.id.left_ll);
        rightLL = (LinearLayout) relativeLayout.findViewById(R.id.right_ll);
        imageView = (ImageView) relativeLayout.findViewById(R.id.image);
        tvName = (TextView) relativeLayout.findViewById(R.id.tv_product_name);
        tvPrice = (TextView) relativeLayout.findViewById(R.id.tv_price);
        tvTags = (TextView) relativeLayout.findViewById(R.id.tv_tags);
        etAmount = (EditText) relativeLayout.findViewById(R.id.et_result_amount);
        actvName = (AutoCompleteTextView) relativeLayout.findViewById(R.id.actv_result_name);
        spTags = (Spinner) relativeLayout.findViewById(R.id.sp_tags);
        btnTags = (ImageButton) relativeLayout.findViewById(R.id.btn_tags);
        spaceImage = (Space) relativeLayout.findViewById(R.id.filler_space_image);
        spacePrice = (Space) relativeLayout.findViewById(R.id.filler_space_price);
    }
}

PS: I know most people put all View-elements in the Holder one by one, and I used to have this. But getting them all from the layout or one by one doesn't make any difference in the problem I'm facing.

PSS: Those three update-methods re-apply the new data from the OrderedProductItem fields in the Spinner / EditTexts / AutoCompleteTextView. I've placed them in the ChecklistActivity since I also use the same method there. This also doesn't make any difference in the problem I'm facing, whether I put them directly in the getView method or call them like I do here.


Solution

  • May I suggest you change your code a little. My ListView's getView() always looks like this and I have no problems with it.

    @Override
    public void getView(int position, View convertView, ViewGroup parent) {
    
        View view = convertView;
        MyViewHolder holder = new MyViewHolder();
    
        if (view == null) {
    
            view = inflater.inflate(layoutResourceId, null);
    
            holder.textView1 = (TextView) view.findViewById(R.id.my_tv1);
            holder.button1 = (Button) view.findViewById(R.id.my_btn1);
            // More views
    
            view.setTag(holder);
        } else {
            holder = (MyViewHolder) view.getTag();
        }
    
        holder.textView1.setText("Some text");
        holder.button1.setText("Some button text");
        holder.button1.setEnabled(buttonEnabled);
    }
    
    public static class MyViewHolder {
        public TextView textView1;
        public Button button1;
        // ... and so on
    }