Best Practice and issues with ListView in Android Xamarin

Listing information is an fundamental requirement in any mobile application development. Android developers usually prefers Listview control for this.It makes easier user interaction to data model.

In the previous post  I explained how to Requesting REST Webservice with JSON in C# Xamarin Android, How to use Google Place API with Autocomplete in Xamarin Android.

I have observed some of the below issues with ListView operation.
->Listview Item click showing wrong view position.
->ItemClick event fires more then once.
->Out of memory  exception.
Let us proceed by creating new android project, Bind the Listview by using custom adapter. Fill the list with sample data of type "ItemClass" and passed to custom adapter class "ItemAdapterClass". 
  
ListView listViewItem;
  ItemAdapterClass objItemAdapter; 
  Listk < ItemClass >  lstItem; 
void BindListView() 
  { 

    lstItem = new List < ItemClass >  ();
    FillList ();
    objItemAdapter = new ItemAdapterClass (this, lstItem);
    listViewItem.Adapter = objItemAdapter; 
  }
  void FillList()
  {
   ItemClass objItem; 
   for (int i = 0; i  < 2000 ; i++)
   {  
    objItem = new ItemClass ();
    objItem.ItemName =string.Format( "Item_{0} ",i);
    objItem.ItemImage = "Image name";
    lstItem.Add (objItem); 
   } 
  }
public class ItemClass
 {

  public string ItemName { get; set;}
  public string ItemImage{ get; set; }
 }
On run of application,screen looks like this :
 In my custom adapter class "GetView" method is as follows,
public override  View GetView (int position,  View convertView,  ViewGroup parent)
  { 
   View rowView = convertView;
   //reuse view
   if (rowView == null) { 
    rowView = _context.LayoutInflater.Inflate (Resource.Layout.ItemCustomLayout, parent, false); 
    TextView txtTemName = rowView.FindViewById<TextView> (Resource.Id.lblItemName);
    ImageView imgItem = rowView.FindViewById<ImageView> (Resource.Id.imgItem);
    CheckBox chkItem = rowView.FindViewById<CheckBox> (Resource.Id.checkitem);
    
    chkItem.Click += delegate(object sender, EventArgs e) {
     Toast.MakeText(_context,string.Format( "Position:{0}, {1}",position, _lstItem [position].ItemName),ToastLength.Long).Show();
    };
   } 
   txtTemName.Text = _lstItem [position].ItemName;  
   imgItem.SetImageResource (Resource.Drawable.imgItem); 
   return rowView;
  }
Here my emulator screen takes 11 view row's. Within that 11 rows chkItem.Click shows the correct position.

But on scrolling to down [below 11 row]and again chkItem.Click returns wrong position.


Even calling chkItem.Click method after influating View row ( the if statement) has no change.
so it shows that view row recycling is not properly done. Correct position is not attached to view row.

Following below method helped to get ride of these issues :

 -> Defined View holder class to declare all the Control, Viewholder is an inner class which holds reference to the relevant rows.
This reference can be attached to the rowView(custom row) by using the "Tag" property.
And when reusing the row get the instance of the viewholder class attached previously to the rowView by using the same property "Tag". This approach  approximately 15 % faster then the using in-line findViewById() method.

//Adapter class GetView()
 
 public override  View GetView (int position,  View convertView,  ViewGroup parent)
  { 
    View rowView = convertView;
  
   if (rowView == null) { 
    rowView = _context.LayoutInflater.Inflate (Resource.Layout.ItemCustomLayout, parent, false); 

    viewHolder = new ViewHolderItem ();
    viewHolder.txtTemName = rowView.FindViewById < TextView >  (Resource.Id.lblItemName);
    viewHolder.imgItem = rowView.FindViewById < ImageView >  (Resource.Id.imgItem);
    viewHolder.chkItem = rowView.FindViewById < CheckBox >  (Resource.Id.checkitem); 
     
    rowView.Tag = viewHolder; //attaching viewholder reference to row
   } 
   else
   {
    viewHolder = (ViewHolderItem)rowView.Tag; //during row re-use get the instance of the view holder
   } 
   viewHolder.txtTemName.Text = strX;  
   viewHolder.imgItem.SetImageResource (Resource.Drawable.imgX);  
   return rowView;
  }

  class ViewHolderItem :Java.Lang.Object
  {
   internal   TextView txtTemName;
   internal   ImageView imgItem;
   internal   CheckBox chkItem; 
  }

 
 To solve a wrong position issue,
Method#1:
->During each row binding, Tag the row position to the Control(here check box) which is used to get the click event.
And get that tagged position from the Control during item click operation.
  
 public override  View GetView (int position,  View convertView,  ViewGroup parent)
  { 
    View rowView = convertView; 
   if (rowView == null) { 
    rowView = _context.LayoutInflater.Inflate (Resource.Layout.ItemCustomLayout, parent, false); 

    viewHolder = new ViewHolderItem ();
    viewHolder.txtTemName = rowView.FindViewById < TextView >  (Resource.Id.lblItemName);
    viewHolder.imgItem = rowView.FindViewById < ImageView >  (Resource.Id.imgItem);
    viewHolder.chkItem = rowView.FindViewById < CheckBox >  (Resource.Id.checkitem); 

    viewHolder.chkItem.Click += delegate(object sender, EventArgs e)
    {
     var chkBx= (CheckBox)sender; 

     int pos = (int)chkBx.Tag; //get the tagged position from the Control

      if (lstSelectedItem.Contains (_lstItem [pos].ItemName))
      {
       lstSelectedItem.Remove (_lstItem [pos].ItemName); 
      }
      else
      {
       lstSelectedItem.Add (_lstItem [pos].ItemName); 
      }  
    };

    rowView.Tag = viewHolder; //attaching viewholder reference to row
   } 
   else
   {
    viewHolder = (ViewHolderItem)rowView.Tag; //during row re-use get an instance of the view holder class
   } 

    viewHolder.txtTemName.Text = _lstItem [position].ItemName;  
    viewHolder.imgItem.SetImageResource (Resource.Drawable.imgItem); 
    viewHolder.chkItem.Checked = lstSelectedItem.Contains (_lstItem [position].ItemName) ? true :false ; 

    viewHolder.chkItem.Tag=position; //here attached row position to control chkItem

    return rowView;
  }
  class ViewHolderItem :Java.Lang.Object
  {
   internal   TextView txtTemName;
   internal   ImageView imgItem;
   internal   CheckBox chkItem; 
  }

Method#2:
-> Another method is by using  "event action". This triggers the method in Activity class from the adapter class getview method.
Here Action is a standard delegate that accepts parameters and doesn't return value. It's used to represent an action.[ http://stackoverflow.com/questions/2834094/what-is-actionstring].
In other word event Action<type> is used to notify the subscriber along with defined <type> from publisher.

//Adapter class GetView()
   
 internal event Action<string> ActionImgSelectedToActivity; 
 public override  View GetView (int position,  View convertView,  ViewGroup parent)
  { 
   View rowView = convertView;
   if (rowView == null) { 
    rowView = _context.LayoutInflater.Inflate (Resource.Layout.ItemCustomLayout, parent, false); 
    viewHolder = new ViewHolderItem ();
    viewHolder.txtTemName = rowView.FindViewById<TextView> (Resource.Id.lblItemName);
    viewHolder.imgItem = rowView.FindViewById<ImageView> (Resource.Id.imgItem);
    viewHolder.chkItem = rowView.FindViewById<CheckBox> (Resource.Id.checkitem); 
    viewHolder.Initialize (rowView);//to listen click event from viewholder class 
    rowView.Tag = viewHolder;
   } 
   else
   {
    viewHolder = (ViewHolderItem)rowView.Tag; 
   }
  //subscribe to click event from viewholder class
   viewHolder.eventHandlerImgViewSelected= () =>
   {
  //publish click event to activity class along with itemname
    if(ActionImgSelectedToActivity!=null)
     ActionImgSelectedToActivity(_lstItem[position].ItemName);  
   };
 
   viewHolder.txtTemName.Text = _lstItem [position].ItemName;  
   viewHolder.imgItem.SetImageResource (Resource.Drawable.imgItem); 
   return rowView;
  }

//viewholder class
 
class ViewHolderItem :Java.Lang.Object
 {
   internal   TextView txtTemName;
   internal   ImageView imgItem;
   internal   CheckBox chkItem;  
   //to publish ImgView click event to Adapter GetView()
   internal event EventHandler eventHandlerImgViewSelected;
   internal void Initialize(View view)
   {
    imgItem=view.FindViewById<ImageView> (Resource.Id.imgItem);

    //to publish ImgView click event to Adapter GetView()
    imgItem.Click += (object sender, EventArgs e) => eventHandlerImgViewSelected ();  
   } 
 }
Activity class BindListView():
 
void BindListView() 
  { 
    lstItem = new List<ItemClass> ();
    FillList ();
   if(lstItem!=null && lstItem.Count >0)
   {
    if ( objItemAdapter != null )
    {
     //Un-subscribe to click event
     objItemAdapter.ActionImgSelectedToActivity -= SelectedItem;
     objItemAdapter = null;
    }
   
    objItemAdapter = new ItemAdapterClass (this, lstItem);
    //subscribe to click event from Adapter class GetView() method 
    objItemAdapter.ActionImgSelectedToActivity += SelectedItem;
    listViewItem.Adapter = objItemAdapter; 
   }
  }

  void SelectedItem( string strItemName)
  {
   Toast.MakeText ( this , string.Format("From Activity :{0}",strItemName ), ToastLength.Short ).Show ();
  }
Have a look at the complete code here : https://github.com/suchithm/SampleListView/ 
These are the some issues and the solution i find out during my coding. Hope it might helpful to somebody who encounter with the same issues.

6 comments:

  1. This comment has been removed by a blog administrator.

    ReplyDelete
    Replies
    1. Thanks for your comment,but I don't allow comments with Ads

      Delete
  2. Hi Suchith Madavu,

    I'm having the same problems with xamarin Android lists. I just tried your first method to get the things done but i've got a cast roblem when i try to use
    viewGroup = (ViewGroupItem)rowView.Tag;
    It's telling me "Specified Cast is not valid".

    Have you got any idea?

    This is my code

    public override View GetView(int position, View convertView, ViewGroup parent)
    {
    var item = items[position];

    View rowView = convertView; // re-use an existing view, if one is available
    if (rowView == null)
    {
    rowView = context.LayoutInflater.Inflate(Resource.Layout.showFamilyFoodItem, null);
    viewHolder = new ViewHolderItem();
    viewHolder.txtName = rowView.FindViewById(Resource.Id.txtFoodFamilyItem);
    viewHolder.lin = rowView.FindViewById(Resource.Id.linFamilyFood);

    viewHolder.lin.Click += delegate (object sender, EventArgs e)
    {
    int pos = (int)((LinearLayout)sender).Tag;

    Variables.selected_foodItem = items[pos];

    Variables.conto.Insert(0, new FoodConto()
    {
    item = Variables.selected_foodItem,
    Quantità = 1,
    Portata = Variables.portata,
    Aggiunte = null,
    Comanda = 0,
    Separato = Variables.separato
    });

    Thread.Sleep(300);

    lsConto.Adapter = new ContoListAdapter(context, Variables.conto.FindAll(x => x.Separato == Variables.separato), lsConto);
    };

    //attaching viewholder reference to row
    rowView.Tag = viewHolder;
    }
    else
    {
    //during row re-use get an instance
    viewHolder = (ViewHolderItem)rowView.Tag;
    }

    viewHolder.lin.Tag = position;
    viewHolder.txtName.Text = String.Concat(item.Name, " - ", item.Price.ToString("n2"));
    return rowView;
    }

    class ViewHolderItem : Java.Lang.Object
    {
    internal TextView txtName;
    internal LinearLayout lin;
    }

    ReplyDelete
  3. Hi, do you have view holder class with name "ViewGroupItem", as it is not there in ur posted code here.

    ReplyDelete
    Replies
    1. viewHolder = (ViewHolderItem)rowView.Tag;
      This line of code tells me "Specified cast is not valid".
      Sorry my bad for what i wrote before.

      Delete
  4. Just Found the solution. The problem was that set the click event in my row LinearLayout, after i set ot in the txtName and delete the LinearLayou reference it worked perfect. Thanks for your help.

    ReplyDelete