5

I'm trying to wrap my head around getView() and I don't think there's any topic in Android that creates more confusion and questions on StackOverflow and elsewhere. Everyone wants to know why it gets called so many times or in what order or not at all, but as Romain Guy says here , " there is absolutely no guarantee on the order in which getView() will be called nor how many times."

So I have a different question: I don't understand the convertView parameter.

I have a list of 15 items, 11 of which can fit on the screen. The very first time my app starts up getView() is called 48 times.

convertView is null for position 0 on the first call, the non-null for positions 1-11, then non-null for position 0 and null for positions 1-11, then null for position 0 and non-null for positions 1-11, and finally non-null for positions 0-11.

Could someone please explain why/when convertView is null versus non-null, how/why it starts off non-null for most positions, and why the same positions seem to bounce back and forth between these two states?

References to good tutorials, written in clear English, that explain convertView in detail would also be appreciated.

PS - my tests were done on a device running Android 2.3.5, if that matters. I know Google has changed ListActivity/adapter/getView stuff several times since then.

Per request, I'm including the Adapter code (I've obscured some proprietary names). Unfortunately I can't answer any "why did you do that?" questions, since I didn't write it

protected class PLxxxAdapter extends BaseAdapter {

    public PLxxxAdapter(Context c) {
    }

    @Override
    public int getCount() {
        return listItems.size();
    }

    @Override
    public Object getItem(int position) {
        return listItems.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        boolean select;

        if (convertView == null) {              
            LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = vi.inflate(R.layout.PLxxxitem, null);
            //This is still needed even though we point to an XML description
            convertView.setLayoutParams(new ListView.LayoutParams(
                    ListView.LayoutParams.MATCH_PARENT,
                    ListView.LayoutParams.MATCH_PARENT));
            holder = new ViewHolder();

            //Get the views of the row
            holder.itemView = (TextView)convertView.findViewById(R.id.post);
            holder.cV1 = (CheckedTextView)convertView.findViewById(R.id.check1);

            //Init the 'confirm' box listener
            holder.cV1.setCompoundDrawablesWithIntrinsicBounds(0, R.layout.smallcb, 0, 0);
            holder.cV1.setOnClickListener(new ConfBoxListener());

            convertView.setTag(holder);
            holder.cV1.setTag(holder);  //These views need tags for onClick()
        }
        else {
            holder = (ViewHolder)convertView.getTag();  // convertview NOT null
        }

       try  {
          int liSize = listItems.size();
          if (position < liSize)  {
             holder.itemView.setText(listItems.get(position));
          }
       }
       catch (Exception e)  {
              Log.e ("PLxxxActivity.getView Crash", "details " + e);
       }
        holder.cV1.setChecked(confirmed.get(position));
        select = selected.get(position);

        if (select == true)   {
            convertView.setBackgroundResource(R.color.colBlue);
        }               

        else
            convertView.setBackgroundResource(R.color.colGrey);
        holder.position = position;
        if (RemoteControlActivity.confCBs == true)
            holder.cV1.setVisibility(View.VISIBLE);
        else
            holder.cV1.setVisibility(View.INVISIBLE);

        return convertView;
    }  // end getView
}  //end class PLxxxAdapter
Community
  • 1
  • 1
user316117
  • 7,971
  • 20
  • 83
  • 158
  • 1
    As stated in my answer, the fact that your `getView` method is called 48 times, might be an issue in your code. If you could show how you set your adapter, and how you handle it, maybe we could help you with that as well. – nhaarman May 09 '14 at 18:30
  • 1
    @user1199931 It will not *always* be non-null after that. It will be null for at least the number of Views that can be displayed on screen (however, this gets more complicated when you get views that may vary in size; or multiple item view types) – Kevin Coppock May 09 '14 at 18:31

3 Answers3

6

The first x times, where x is a number near the number of items visible on the screen, convertView is null. You need to instantiate a new View to return.

When you scroll down, an existing View is pushed upwards out of sight. Instead of destroying it, it can now be reused. You'll notice that, just before a new View is pushed in from below, your getView method is called, with a valid convertView. This is exactly that View that was pushed out of sight before (or maybe another one, there is some additional logic)!

Therefore, instead of re-instantiating your View, which is costly, you can reuse the View and adapt it to the new item it represents. You will often see something like:

View view = convertView;
if(view == null){
    view = LayoutInflater.from(getContext()).inflate(...);
}

// 'bind' view

return view;

The fact that your getView method is called 48 times on startup, might actually be an issue with your code.

nhaarman
  • 98,571
  • 55
  • 246
  • 278
  • You said "The first x times, where x is a number near the number of items visible on the screen, convertView is null" - that's what I thought, too, but it's plainly not the case, as I indicated in my question. Hence my confusion. – user316117 May 09 '14 at 18:30
  • @user316117 See my comment on your question :) – nhaarman May 09 '14 at 18:31
  • This is a very good explanation mate. I am glad you wrote it down. Thanks. – Kaveesh Kanwal Apr 27 '15 at 15:23
  • I have the same problem but in my case I have edittext in my listview. So, on generating a new row of position x by `notifyDataSetChanged`, both the 2 edittexts get the focus and the same data too. Pls reply fast – Daksh Agrawal Aug 19 '17 at 11:43
0

The convertView parameter is a recycled instance of some View that you previously returned from getView(). The first time that getView() is called, convertView is guaranteed to be null, as you have not yet created a View to be recycled. When one of your Views in the list have been scrolled offscreen and are no longer visible to the user, they are removed from the ViewGroup and added to the internal "recycling bin". You will then start receiving these recycled views as the convertView parameter, once they are available.

You should always check if convertView is non-null, and if it is, you should attempt to re-use it. For example, if your getView() inflates a TextView, it is safe to cast convertView to a TextView instance if it is non-null. Then you should update its text to match whatever represents your item at the current position (getItem(position)).


EDIT:

The only reason you need this:

    convertView = vi.inflate(R.layout.PLxxxitem, null);
    //This is still needed even though we point to an XML description
    convertView.setLayoutParams(new ListView.LayoutParams(
            ListView.LayoutParams.MATCH_PARENT,
            ListView.LayoutParams.MATCH_PARENT));

is because of the way you're inflating the view. Instead of passing null, pass the parent with attachToRoot = false, i.e.:

convertView = vi.inflater(R.layout.PLxxxitem, parent, false);

Regardless, I don't think your problem lies in your adapter.

Kevin Coppock
  • 133,643
  • 45
  • 263
  • 274
  • There is no scrolling. As I said, this is when the app is first started up - the device is just sitting on the table, not being touched. Position 0 is null but positions 1-11 are non-null. How is this possible? FWIW - when it's all over and getView has been called 48 times the screen is properly populated. – user316117 May 09 '14 at 18:37
  • Are you calling notifyDatasetChanged() on your adapter somewhere? That will trigger a redraw of all the views in the list. Also if your layout changes (are you doing a requestLayout() somewhere?). TL;DR: show some code. – Kevin Coppock May 09 '14 at 18:38
  • But if it triggers a redraw of all the views won't that also cause getView to be called? I'm not concerned with how many times getView() is called (as Romain says, there are no guarantees) - I would like to understand how convertView can be non-null on 11 of the 12 positions right off the bat. It's a big program - what code would you like to see? – user316117 May 09 '14 at 18:50
  • OK I've added the source code. In the Google documentation at [link(http://developer.android.com/reference/android/widget/Adapter.html) they say convertView is _"The old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before using"_ Do they document anywhere what **exactly** they mean by "the old view"? – user316117 May 09 '14 at 19:35
  • They mean a view you previously created in getView(). – Kevin Coppock May 09 '14 at 19:38
  • If that's what they mean then since it's null the first time getView is called and non-null the second time, then logically that means the **ONLY** possible view it can be the second time is the one I created on the previous call, correct? – user316117 May 09 '14 at 19:45
  • The only likely scenario I see this happening on is if for some reason your listview initially gets measured too small (maybe 0 for some reason; or just small enough to fit one item). You're not animating the size of the ListView are you? I'd post how you're assigning and/or updating the adapter. – Kevin Coppock May 09 '14 at 19:47
  • The first non-null ConvertView does **not** have the same object ID as the one that I returned in the previous getView() call. Is there some other sense in which Google means the "same" convertView? How should I test to see if one convertView is the "same" as another other than object ID? – user316117 May 09 '14 at 20:01
  • `view1 == convertView`? – Kevin Coppock May 09 '14 at 20:03
0

The problem, as hinted by others, is probably not in your adapter. I fairly certain of this because I just fought the same exact problem and came here looking for answers only to be disappointed. In my case, I set my list view's width & height to wrap_content instead of match_parent. When I set it to match_parent I got the desired behavior. It's extremely hard to uncover because the view would grow to accommodate the size of its contents and take up the entire size of its container thus showing multiple list entries, however the system "thought" it could only fit 1 item. So the getView method was being called repeatedly with the same convertView. (It was null only on the 1st call as in your case.) A possible approach (which I have not tried) would be to interrogate the parent list view and check its size. Make sure it has enough room to render multiple rows and adjust accordingly.

Cliff
  • 10,586
  • 7
  • 61
  • 102