Friday, May 24, 2013

Get friendly with listview custom adapter, view recycling, convertview, scrolling problem etc. etc with ListView

So, this post is completely dedicated to listview in android and the very common issue which many of us faces, the very well known LIST VIEW SCROLLING PROBLEM or ROWS REPEATING PROBLEM.

I have tried here to explain about the custom adapter of listview, and all its methods, very specifically getView() method where all the important part of your adapter gets accomplished, with the help of an example.

Everyone  android developer knows, how important is ListView in Android. So, we should have a very good understanding about it, that how it works and how to make it work efficiently for performance optimization of our application.

In brief, listview is a component of Android which is used to represent any information in the form of list on your android device,which is by default scrollable. It is comprised of a number of rows which get populated from the data that user provide. Each row is a distinct view in itself and we can populate it as per our need.

For populating data in listview Adapter is used which is nothing but a component which holds the data and  using that data listview is populated. Adapter broadly can be categorized as Simple and Custom. Simple adapter bound us from generating rows of listview as per our need, as it only allows to populate rows with only text.

Here comes Custom Adapter using which listview can be populated in any manner means we can use number of Android widgets like Textviews, Imageviews, EditTexts, Buttons etc.

As rows can be customized using Custom Adapter, so we need a different layout (layout file) which will be inflated for building the row. Make the layout as per your requirement and then make a CustomAdapter class in your java code, which can extend any of the BaseAdapter, ArrayAdapter or Cursor Adapter. For now I will not go in detail of these adapters. Implement methods of the adapter in the CustomAdapter class like getCount(), getItem(), getItemId(), and the most important getView() .

In the getView method all the rows are inflated, and data is populated. If getView() is not properly implemented, it will cost a great performance loss.

Their are majorly two ways in which getView() can be implemented-

1) Just get reference to all your views of the row by findViewbyId() method and fill them with the data. But it will cost a major performance issue.(Memory and scrolling issues)

2) Use the convertView parameter of getView() and use a viewholder along side. This is the most efficient way to implement getView() which directly affects the listview performance, by reducing memory consumption and making scrolling also smooth.

View Recycling and ConvertView

Que. First of all How ListView Works ?

Ans. When for the first time listview appears on screen and suppose listview has total number of 10 rows but  as per the device screen size, initially it will show only some number of rows that can fit in the screen at once. Suppose it can show 5 rows initially. So what happens when the listview is scrolled ?
         When the listview is scrolled then also it will show only the 5 rows at once but with rows changed. When listview is scrolled up or down, the row is recycled and reused. Suppose, you scrolled list upward so that a new row from the bottom comes and the row which was at the first position disappears from the screen. Here comes the Recycler. The row which was disappeared from the top, actually that row only appears at the bottom as a new row but with the same state as it was earlier and this view is only called as the CONVERT VIEW, which get recycled means reused in listview. Here user has to take care of updating the data of the ConvertView as it has the previous data. Everytime you scroll, convertview is used. Only at the initial screen it is null. In this way views are reused in listview.

Now with an example i'll show getView() implementation practically, which gets data from server and accordingly update the list.

I'll just briefly explain that here is my CustomAdapter class and all its methods. Actually this adapter is inflating the lisview with its row with content like an ImageView, some TextViews and a Button. The activity in which this list is used,shows friendlist when user start a search operation in his application and this list appears with friends and their friendship status as label on Button text as "Add friend", "Friend","Request Sent". If its AddFriend, onclicking its label(status) changes to Request Sent.



class ListViewCustomAdapter extends BaseAdapter {
private static final String REQUEST = "Requested";
private static final String ACCEPT = "Accepted";
private static final String RECEIVE = "Received";
private Activity context;
private ArrayList<FriendList> friendList;
private SessionManager sessionManager;
private String authToken;
private ProgressDialog progressDialog;
ViewHolder mViewHolder;
//private HashMap<Integer, String> buttonTextMap;

public ListViewCustomAdapter(Activity activity,

ArrayList<FriendList> _friendList) {

this.context = activity;

this.friendList = _friendList;
sessionManager = new SessionManager(context);
authToken = sessionManager.getAuthorizationKey();

}


public int getCount() {

// TODO Auto-generated method stub
return friendList.size();
}

public Object getItem(int position) {

// TODO Auto-generated method stub
return friendList.get(position);
}

public long getItemId(int position) {

// TODO Auto-generated method stub
return position;
}

public View getView(final int position, View convertView, ViewGroup parent) {


LayoutInflater layoutInflater = (LayoutInflater) context

.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (convertView == null) {
convertView = layoutInflater.inflate(
R.layout.friend_list_view_item, null);

mViewHolder = new ViewHolder();

mViewHolder.profilePicture = (ImageView) convertView
.findViewById(R.id.friendPicureImageView);

mViewHolder.friendName = (TextView) convertView

.findViewById(R.id.firstNameTextView);

mViewHolder.email = (TextView) convertView

.findViewById(R.id.emailTextView);

mViewHolder.gender = (TextView) convertView

.findViewById(R.id.genderTextView);

mViewHolder.addButton = (Button) convertView

.findViewById(R.id.addFriendButton);

convertView.setTag(mViewHolder);


} else {

                         mViewHolder = (ViewHolder) convertView.getTag();
Log.e("FriendList", "status ===== "+friendList.get(position).getFriendRequestStatus()+" position: "+position);

}

if (friendList.get(position).getFriendRequestStatus()

.equalsIgnoreCase("null")) {
Log.e("FriendList", "status ===== "+friendList.get(position).getFriendRequestStatus());
mViewHolder.addButton.setText("Add Friend");

}

byte[] imageByteArray = Base64.decode(friendList.get(position)
.getFriendProfilePic(), Base64.DEFAULT);

mViewHolder.profilePicture.setImageBitmap(BitmapFactory

.decodeByteArray(imageByteArray, 0, imageByteArray.length));

if (friendList.get(position).getFriendFirstName()
.equalsIgnoreCase("null")) {

                   mViewHolder.friendName.setText(friendList.get(position).getFriendLastName());
                 } 
                
                else if (friendList.get(position).getFriendLastName().equalsIgnoreCase("null")) {

                  mViewHolder.friendName.setText(friendList.get(position).getFriendFirstName());

              
                else if (friendList.get(position).getFriendLastName() .equalsIgnoreCase("null")
&& friendList.get(position).getFriendFirstName().equalsIgnoreCase("null")) {

mViewHolder.friendName.setText("No Name");



              else {
mViewHolder.friendName.setText(friendList.get(position).getFriendFirstName()
+ " "+ friendList.get(position).getFriendLastName());
}

if (!friendList.get(position).getFriendEmail().equalsIgnoreCase("null")) {


mViewHolder.email.setText(friendList.get(position).getFriendEmail());
}

if (!friendList.get(position).getFriendGender()
.equalsIgnoreCase("null")) {

                      if (friendList.get(position).getFriendGender().equalsIgnoreCase(Constants.MALE)){

mViewHolder.gender.setText(Constants.SET_MALE);

else if (friendList.get(position).getFriendGender() .equalsIgnoreCase(Constants.FEMALE)) {
mViewHolder.gender.setText(Constants.SET_FEMALE);
}
}

if (friendList.get(position).getFriendRequestStatus()

.equalsIgnoreCase(REQUEST)) {

mViewHolder.addButton.setText("Request Sent");
mViewHolder.addButton.setEnabled(false);
}

                 else if (friendList.get(position).getFriendRequestStatus()
.equalsIgnoreCase(ACCEPT)) {

  mViewHolder.addButton.setText("Friend");
mViewHolder.addButton.setEnabled(false);


                else if (friendList.get(position).getFriendRequestStatus()
.equalsIgnoreCase(RECEIVE)) {

mViewHolder.addButton.setText("Accept");
mViewHolder.addButton.setEnabled(true);
}

Log.d("FriendList", "position in getview===== " + position);

                mViewHolder.addButton.setTag(position);

                mViewHolder.addButton.setOnClickListener(new OnClickListener() {

public void onClick(View v) {


int which = -1;

Object obj = v.getTag();
if (obj instanceof Integer) {
which = ((Integer) obj).intValue();
}

Button button = (Button)v;

  if (button.getText().toString().equalsIgnoreCase("Accept")) {
Intent intent = new Intent(context,NotificationsActivity.class);

context.startActivity(intent);
context.finish();

} else {
int id = button.getId();
addFriend(button, friendList.get(position).getFriendUserId(), which);

}


}

});
return convertView;
}

static class ViewHolder {

TextView friendName;
TextView email;
TextView gender;
ImageView profilePicture;
Button addButton;
}

private void addFriend(final Button _button, final String userId,

final int _position) {
final JSONObject jsonObject = new JSONObject();

Log.e("FriendListActivity", "position in addFriend=== " + _position);


try {

jsonObject.put("authToken", authToken);
jsonObject.put("targetFriendId", userId);
jsonObject.put("requestType", "FriendRequest");
jsonObject.put("status", "Requested");

} catch (JSONException e) {

// TODO Auto-generated catch block
e.printStackTrace();
}

final Handler handler = new Handler() {


@Override

public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
progressDialog.dismiss();
if (msg.what == 1) {

                                       friendList.get(_position).setFriendRequestStatus("Request");

                                       FriendListActivity.adapter.notifyDataSetChanged();

                                        _button.setText("Request sent");
_button.setEnabled(false);

                                         Toast.makeText(context, "Request sent successfully.",
Toast.LENGTH_LONG).show();
} else {
Toast.makeText(context, "Request unsuccessfull.",
Toast.LENGTH_LONG).show();
}

}

};

progressDialog = ProgressDialog.show(context, "", "Loading...");


new Thread() {

@Override
public void run() {

String response = DoFriendRequest

.makeHttpPostRequest(jsonObject);
Message message = new Message();
if (response != null) {
message.what = 1;
handler.sendEmptyMessage(message.what);
} else {
handler.sendEmptyMessage(0);
}

}

}.start();

}


}


If you have thoroughly gone through the code, you must have understood what I was trying to explain.
getView() method gets called, whenever you scroll or update some UI. So when I click button with status
AddFriend, 3 things have to be done -

1) Change label of button to Request Sent.
2) Update adapter datalist.
3) Update status at server.

So all these actions are performed.

By referring above code you will not face, Rows Repeating Problem .

Now FULLSTOP.

Please feel free to comment or  ask anything regarding this post if any queries are there. This is my first blog post ever, so sorry if I am not understandable at any point.