I'm using a listView in my app where every every row of listview gets its data from a different webservice. Currently, I'm doing this in the following fashion:
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
...
...
DownloadData downloadData = new DownloadData(viewHolder);
downloadData.execute();
return convertView;
}
class DownloadData extends AsyncTask<Void,Void,Void> //Downloads and update viewHolder
{
.....
}
It runs perfectly.
However, I feel this isn't a good design since threading isn't supposed to be done inside an adapter. Is there a better approach to this ?
P.S: There is a constraint on my webservice and I can't alter it to solve this issue
You definitely do not want to be doing this in getView(). getView() is really just responsible for turning some objects into views, it's not responsible for getting your data.
I would make sure I had all the data I needed before or even during population of the listview.
In other words, outside this adapter why don't you call all your async methods and have them return the right objects to pass into the adapter?
You can wait till they are all done, or call them in parallel and update the adapter as each one finishes, and call notifyDatasetChanged().
Related
I am trying to speed up an app as much as possible by calling findViewById() as little as possible in the Adapter class.
I've heard of a "way" where I can create a HashMap<T> containing View instances and integers as values, and use it along or together with adapter's ViewHolder inner class. As you can see, I heard very little about this and did not have chance to ask more questions about this "way".
I have been searching for the solution on the Internet, but I am either searching using the wrong keywords or I am not recognizing solutions as such.
Can anyone direct me in a right way? Maybe someone writing a small sample on how to accomplish this?
EDIT 1
I don't have any issues with my current ListView as it's already using ViewHolder pattern. The reason for this question was because I heard that you can actually gain more speed using HashMap pattern than using ViewHolder pattern. I know this is vague description, but that's why I came here to ask.
Sharing a code would not help at all, because it's just a regular adapter using ViewHolder pattern, just like the one in the links you shared. There is no any experimenting or something that made the adapter slow.
Saying it again, the main reason to ask this was to find our if there is a pattern faster than the ViewHolder
The way to reduce the amount of calls to findViewById is to "recycle" Views with ViewHolder. The idea behind is to keep inflated views and reuse them when needed - and since they already have ViewHolders you don't need to make the findViewById calls.
As #nhaarman described in the last part of his answer the way to do it for listviews is in the getView to reuse the convertView if it's not null and using the referenced view in the ViewHolder to update them.
An approach how to create ViewHolder and store them in the View by setTag is described in the first link #nhaarman provided.
Then in the getView function if convertView isn't null call the getTag inorder to get the ViewHolder back.
In another note, you should look into RecyclerView which is a listview that enforces the ViewHolder pattern.
You should be using the ViewHolder paradigm which is most easily implemented with RecyclerView when Adapters are involved. Online sample code available here and GitHub sample cloneable via AndroidStudio directly or via Github.
The HashMap solution will work, but why incur a HashMap lookup when you can simply hold a reference to the object? Also, RecyclerView natively handles different View TYPEs so you don't have to roll your own solution when you have section header rows which look differently from data rows.
===============================================
A more detailed explanation of RecyclerView follows based on snippets from the sample I linked to.
Have your CustomAdapter extend RecyclerView.
public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder> {
Create a Custom ViewHolder class. It should almost always be a static class if embedded within your CustomAdapter.
public static class ViewHolder extends RecyclerView.ViewHolder {
private final TextView textView;
public ViewHolder(View v) {
super(v);
// Define click listener for the ViewHolder's View.
v.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Log.d(TAG, "Element " + getPosition() + " clicked.");
}
});
textView = (TextView) v.findViewById(R.id.textView);
Notice that the ViewHolder finds the views in the constructor since a ViewHolder instance is tied to a View instance. The ViewHolder should have references to any UI elements that you plan to update in an onBind callback. The ViewHolder object is then associated with the View by RecyclerView when you include the following code in your CustomAdapter.
// Create new views (invoked by the layout manager)
#Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
// Create a new view.
View v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.text_row_item, viewGroup, false);
return new ViewHolder(v);
}
Notice how the onCreateViewHolder callback is given a viewType parameter. The current code always returns the same type of custom ViewHolder, but it could be providing different ViewHolder classes based on the type (this is how you support rows that support different views). The following code is then how you update your UI when the view is bound to a dataset.
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(ViewHolder viewHolder, final int position) {
Log.d(TAG, "Element " + position + " set.");
// Get element from your dataset at this position and replace the contents of the view
// with that element
viewHolder.getTextView().setText(mDataSet[position]);
}
With these pieces in place RecyclerView will create and reuse your Views AND ViewHolders by corresponding type reducing view hierarchy look ups and potentially making your app much less "janky".
I have a custom list view which is being popluated via an array adaptor.
Each item/row contains three buttons and some related textViews.
All elements in a row describe the details for a device on the cloud. So data is fetched from the cloud and then the list is populated. No. of rows is equal of the number of devices.
Everything was fine till I added the feature for a periodic update for the items.
The problem is that after each periodic update it over writes the data for a device in the wrong row.
I tried two ways to refresh each row.
I kept a map for (DeviceID and view) and then based on the deviceId
i would get the view and update it. Now,this didn't work as the views are reused and so as i scroll
down, basically the same view is reused as shows the new data. And
so the map entry of the previous device is over written with the new
one.
I tried to directly call getView() and pass the position but that
also didn't work.
I understand that the views are reused so there is no way to know exactly which view is associated with a deviceID.
But could some please help me figure out how to update the correct view with the correct data?
Thanks.
If you are using Holder pattern, then there is a way to do this.
Step 1: Add one attribute i.e. position to Holder.
private class ViewHolder {
....
....
int position;
}
Step 2: Initialise the holder position into getView()
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
...
holder.position = position;
}
....
}
Step 3: Check holder position and view's position values. If both are same then do your task.
if (mHolder.position == mPosition) {
// This is you required row. Do your task.
}
Read Async loading for more details.
Documents say:
When the content for your layout is dynamic or not pre-determined, you
can use a layout that subclasses AdapterView to populate the layout
with views at runtime. A subclass of the AdapterView class uses an
Adapter to bind data to its layout.
But most of tutorials are in about ListView,GridView,Spinner and Gallery.
I'm looking to extend a subclass directly from AdapterView. I have to create a custom view that it's content is dependent to an adapter.
How can I do this, and what methods must be overridden?
First, you should be absolutely sure that AdapterView is what you want, because not all "dynamic or not pre-determined" views can be implement via AdapterView. Sometimes you'd better create your view extending ViewGroup.
if you want to use AdapterView, take a look at this really nice example. There are a lot of custom views with adapter on GitHub. Check out this one (extends ViewGroup).
This may not be a total answer to your question but i am showing you most probably a starting point or pointer which can guide you:
Sony Developer Tutorials - 3D ListView
ListView extends AbsListView which in turn extends AdapterView<ListAdapter>. So if you absolutely must implement such a custom view from scratch, you could have a look at the source code of those classes:
ListView
AbsListView
AdapterView
But beware, that's quite a task. Perhaps it may be sufficient to use one of the existing classes and tweak the look.
Deriving from AdapterView can work, but it may not be as beneficial as you hope. Some of the infrastructure provided by AdapterView is package-private, meaning we don't have access to it.
For example, AdapterView manages the selected item index for AbsListView and ListView. However, because methods like setNextSelectedPositionInt(int position) (which is the only path to setting mNextSelectedPosition) are package-private, we can't get to them. AbsListView and ListView can get to them because they're in the same package, but we can't.
(If you dig into the AdapterView source you'll find that setNextSelectedPositionInt() is called from handleDataChanged(). Unfortunately handleDataChanged() is also package-private and is _not_called from anywhere else within AdapterView that could be leveraged to enable setting position.)
That means that if you need to manage selected position, you'll need to recreate that infrastructure in your derived class (or you'll need to derive from ListView or AbsListView...though I suspect you'll run into similar problems deriving from AbsListView). It also means that any AdapterView functionality that revolves around item selection likely won't be fully operational.
You could create something like this :
public class SampleAdapter extends BaseAdapter {
public SampleAdapter() {
// Some constructor
}
public int getCount() {
return count; // Could also be a constant. This indicates the # of times the getView gets invoked.
}
public Object getItem(int position) {
return position; // Returns the position of the current item in the iteration
}
public long getItemId(int position) {
return GridView.INVALID_ROW_ID;
}
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
view = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.some_layout, null);
view.setLayoutParams(new GridView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
view.setBackgroungColor(Color.RED);
return view;
}
}
And this could be invoked like :
GridView sampleView = (GridView) linearLayout.findViewById(R.id.sample_layout);
sampleView.setAdapter(new SampleAdapter());
I am new to Android development and reading through some example code. I have copied one method from the sample code in an Adapter class (derived from ArrayAdapter), the derived class has a checkbox in addition to the text view:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View listItem = super.getView(position, convertView, parent);
CheckedTextView checkMark = null;
ViewHolder holder = (ViewHolder) listItem.getTag();
if (holder != null) {
checkMark = holder.checkMark;
} else {
checkMark = (CheckedTextView) listItem.findViewById(android.R.id.text1);
holder = new ViewHolder(checkMark);
listItem.setTag(holder);
}
checkMark.setChecked(isInCollection(position));
return listItem;
}
private class ViewHolder {
protected final CheckedTextView checkMark;
public ViewHolder(CheckedTextView checkMark) {
this.checkMark = checkMark;
}
}
The sample code is to optimize the getView by caching the View within a ViewHolder object.
Where I am confused is I thought the convertView, if not null, would be re-purposed and then the View data is populated into it and returned.
If this is the case, then how could the setTag / getTag methods called in the code be relied upon? It would seem that the same object would have to be retrieved in order for it to work?
perhaps view returned from getTag on a subsequent call is for a different list item, and returns the wrong view
Adapters use a RecycleBin. This class allows the ListView to only create as many row layouts as will fit on the screen, plus one or two for scrolling and pre-loading. So if you have a ListView with 1000 rows and a screen that only displays 7 rows, odds are the ListViiew will only have 8 unique Views.
Now to your question using my example above: only eight row layouts and 8 subsequent ViewHolders are ever created. When the users scrolls no new row layouts are ever created; only the content of the row layout changes. So getTag() will always have a valid ViewHolder that references the appropriate View(s).
(Does that help?)
You're on the right track, here's some information that may help make more sense of how ListViews work:
A simple implementation of the getView() method has two goals. The first is inflating the View to be shown on the list. The second is populating the View with the data that needs to be shown.
As you stated, ListViews re-purpose the Views that compose the list. This is sometimes referred to as view recycling. The reason for this is scalability. Consider a ListView that contains the data of 1000 items. Views can take up a lot of space, and it would not be feasible to inflate 1000 Views and keep them all in memory as this could lead to performance hits or the dreaded OutOfMemoryException. In order to keep ListViews lightweight, Android uses the getView() method to marry Views with the underlying data. When the user scrolls up and down the list, any Views that move off the screen are placed in a pool of views to be reused. The convertView parameter of getView() comes from this list. Initially, this pool is empty, so null Views are passed to getView(). Thus, the first part of getView should be checking to see if convertView has been previously inflated. Additionally, you'll want to configure the attributes of convertView that will be common to all list items. That code will look something like this:
if(convertView == null)
{
convertView = new TextView(context);
convertView.setTextSize(28);
convertView.setTextColor(R.color.black);
}
The second part of an implementation of getView() looks at your underlying data source for the list and configures this specific instance of the View. For example, in our test list, we may have an Array of Strings to set the text of the view, and want to set the tag as the current position in the Data of this View. We know which item in the list we're working with based on the position parmeter. This configuration comes next.
String listText = myListStringsArray[position];
((TextView)convertView).setText(listText);
convertView.setTag(position);
This allows us to minimize the amount of time we spend inflating/creating new views, a costly operation, while still being able to quickly configuring each view for display. Putting it all together, your method will look like this:
#Override
public View getView(int position, View convertView, ViewGroup)
{
if(convertView == null)
{
convertView = new TextView(context);
//For more complex views, you may want to inflate this view from a layout file using a LayoutInflator, but I'm going to keep this example simple.
//And now, configure your View, for example...
convertView.setTextSize(28);
convertView.setTextColor(R.color.black);
}
//Configure the View for the item at 'position'
String listText = myListStringsArray[position];
((TextView)convertView).setText(listText);
convertView.setTag(position);
//Finally, we'll return the view to be added to the list.
return convertView;
}
As you can see, a ViewHolder isn't needed because the OS handles it for you! The Views themselves should be considered temporary objects and any information they need to hold onto should be managed with your underlying data.
One further caveat, the OS does nothing to the Views that get placed in the pool, they're as-is, including any data they've been populated with or changes made to them. A well-implemented getView() method will ensure that the underlying data keeps track of any changes in the state of views. For example, if you change text color of your TextView to red onClick, when that view is recycled the text color will remain red. Text color, in this case, should be linked to some underlying data and set outside of the if(convertView == null) conditional each time getView() is called. (Basically, static setup common for all convertViews happens inside the conditional, dynamic setup based on the current list item and user input happens after) Hope this helps!
Edited - Made the example simpler and cleaned up the code, thanks Sam!
I have a JSON string with the multiple instances of the following
Name
Message
Timestamp
Profile photo url
I want to create a ListView where each list will have the above. Which is the better Adapter I need to extend for this particular case, to create a Custom Adapter?
My assumption is that you have parsed the JSON string into an object model prior to considering either case... let's call this class Profile.
Strategy 1
An ArrayAdapter is sufficient with an overriden getView(..) method that will concatenate your multiple fields together in the way you wish.
ArrayAdapter<Profile> profileAdapter = new ArrayAdapter<Profile>(context, resource, profiles) {
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v;
// use the ViewHolder pattern here to get a reference to v, then populate
// its individual fields however you wish... v may be a composite view in this case
}
}
I strongly recommend the ViewHolder pattern for potentially large lists:
https://web.archive.org/web/20110819152518/http://www.screaming-penguin.com/node/7767
It makes an enormous difference.
Advantage 1. Allows you to apply a complex layout to each item on the ListView.
Advantage 2. Does not require you to manipulate toString() to match your intended display (after all, keeping model and view logic separate is never a bad design practice).
Strategy 2
Alternatively, if the toString() method on your representative class already has a format that is acceptable for display you can use ArrayAdapter without overriding getView().
This strategy is simpler, but forces you to smash everything into one string for display.
ArrayAdapter<Profile> profileAdapter = new ArrayAdapter<Profile>(context, resource, profiles)