Lisa Anne
Lisa Anne

Reputation: 4595

Android: best practice to perform asynchronous operations in getView()

Please don't close this, IMHO it is decent and possibly useful programming question.


Please I am reading a lot of stuff, and I am getting confused because I read different opinions and different approaches.

The problem is the following:

in the getView() of an AdapterI need to perform some asynchronous operation, like checking an formation on the web, and update the view based on that.

I used the following approach:

every time getView() is called I start a Thread

but my approach as earned me lots of criticism:

https://stackoverflow.com/a/28484345/1815311

https://stackoverflow.com/a/28484335/1815311

https://stackoverflow.com/a/28484351/1815311

public View getView(int position, View convertView, ViewGroup parent) { 
    ViewHolder holder;            
    if (convertView == null) {                
        //...         
    } 
    else {                
        //...
    }     

    Thread th= new Thread(new Runnable() {

           @Override
           public void run() {
                mActivity.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {

                        CheckSomeInfoOverTheInternet(url, new myCallback {


                            @Override
                            public void onSuccess() {
                            holder.textview.setText("OK");
                            }

                            @Override
                            public void onFailre() {
                            holder.textview.setText("NOT OK!!!!");
                            }    

                        });
                    }
                });


           }
       });

    th.start();

    return convertView;
}  

Please what would be the best practice for doing such a thing?


Please note, I am not looking for a solution to execute the network requests in getView(), but rather, how to updated the view depending on the result on the asynchronous call.

Upvotes: 7

Views: 4939

Answers (7)

denvercoder9
denvercoder9

Reputation: 3021

EDIT

I think this is an interesting question, worth some kind of "canonical" solution

Google I/O 2013 :P I suggest you to watch this Google I/O from 2013. They have clealy explained a lot of these stuff there. All of your questions will be answered there. It's canon.

I have used Volley library here. If you read the docs then you'll see that Volley runs on background threads. So no need to implement your async tasks. Since others have already covered the issues in using Threads I will not talk about those. Let me go into code directly :)

The following has served me well whenever my listviews or gridviews or any other views depend on information from the web:

  1. Create an interface: WebInfoUpdateReceiver.java

    public interface WebInfoUpdateReceiver {
    
        public void receive(Foo [] fooItems);
    
    }
    
  2. Create a class to download stuff: Downloader.java

    public class Downloader {
        private Context mContext;
        private WebInfoUpdateReceiver mReceiver;
    
        public Downloader(WebInfoUpdateReceiver receiver) {
           mReceiver = receiver;
        }
    
        public void downloadStuff() {
        MyStringRequest request = new MyStringRequest(Request.Method.GET,requestUrl, new Response.Listener<String>() {
        @Override
        public void onResponse(String response) {
           // parse the response into Foo[]
    
            mReceiver.update(foo);
                }
            }
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
    
        }
    });
    RequestQueue queue = Volley.newRequestQueue(mContext);
    queue.add(request);
        }
    
    }
    
  3. Now make your activity implement the interface:

    public class Activity extends Activity implements WebInfoUpdateReceiver {
    
    public void receive(Foo [] fooItems) {
         // convert the array  into arrayList
        adapter.insert(arraylist);
        adapter.notifyDataSetChanged();
    }
      }
    

Upvotes: 4

C B J
C B J

Reputation: 1868

So, taking a step back and looking at this completely abstractly, it seems the question you are asking is along the lines of how to manage some potentially long-running action (network or otherwise) that will affect the appearance of a list view item.

Given that assumption, you face two main problems:

  1. Threads are comparatively expensive to start
  2. The views returned by getView() are recycled and can change "item" when the user scrolls the list

Issue 1 can be addressed by using a ThreadPoolExecutor (or whatever other mechanisim you feel like). The idea being that the threads are recycled as well, so they don't take so much time to spin up.

Issue 2 is a little more complicated. You can cancel the thread operation when the view is about to be recycled (there are a few ways of doing this) but you need to decide if losing the effort is acceptable (the user may scroll your view back on the screen and you have to start again). You could store the result of your long-running task against the list item (or some data holder associated with the list item) but you need to be wary of running out of memory (least recently used caching or lru caches can sometimes be useful here). This would allow you to keep your effort and only update your list view if it is still on-screen. A flag in your item data could be used to indicate you already have the data and not load it again.

Sorry, I don't have enough time to go into more detail at the moment. It's bedtime for me :-)

Good luck. I'll write some more if I have time. Kind regards, CJ

Upvotes: 1

Yuriy Kulikov
Yuriy Kulikov

Reputation: 2129

IMHO, the best practice would be not to perform asynchronous operations in getView(). Your Adapter class is only supposed to transform Objects into Views and you should keep it as straightforward as possible.

You can run into trouble if you start an asynchronous task (or a Thread) which references newly created View if the task finishes after the view is not on screen anymore or if it was recycled by the adapter as a result of scrolling.

You should consider performing asynchronous operations outside of the adapter. Start an operation and update the list after it is done. You can update the whole list or some elements when the asynchronous operation is finished.

Operations themselves can be done in AsyncTask of your activity or in a dedicated Service. Avoid creating new Threads because creating threads is expensive and if for some reason you call it too often, you can run out of memory. AsyncTasks use a Thread pool and therefore you will not have this problem.

Upvotes: 1

jzyamateur
jzyamateur

Reputation: 113

Volley library seems to have abstracted out some of the common patters for use...

For finer control over network requests and caching , you could have a look at : http://developer.android.com/training/volley/simple.html#send

Like in the architechture diagram, on making a request, it looks up cache, and in case of misses tries the network request and adds it to cache for later use, Moreover it seems you can provide custom back off-retry request suiting your needs and handle/invalidate cache as and when required.

For in-depth reference you could take a look at - https://android.googlesource.com/platform/frameworks/volley/+/master/src/main/java/com/android/volley

Upvotes: 1

Vamsi
Vamsi

Reputation: 878

You can use something like this:

   public View getView(int position, View convertView,
        ViewGroup parent) {
    ViewHolder holder;

    ...

    holder.position = position;

    new ThumbnailTask(position, holder)
            .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);

    return convertView;
}

private static class ThumbnailTask extends AsyncTask {
    private int mPosition;
    private ViewHolder mHolder;

    public ThumbnailTask(int position, ViewHolder holder) {
        mPosition = position;
        mHolder = holder;
    }

    @Override
    protected Cursor doInBackground(Void... arg0) {
        // Download bitmap here
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (mHolder.position == mPosition) {
            mHolder.thumbnail.setImageBitmap(bitmap);
        }
    }
}

private static class ViewHolder {
    public ImageView thumbnail;
    public int position;
}

Also, for even better performance you need to add interaction awareness to your ListView adapter so that it doesn’t trigger any asynchronous operation per row after, say, a fling gesture on the ListView—which means that the scrolling is so fast that it doesn’t make sense to even start any asynchronous operation. Once scrolling stops, or is about to stop, is when you want to start actually showing the heavy content for each row.

A very good example can be found here: https://code.google.com/p/shelves/

Upvotes: 1

Droidekas
Droidekas

Reputation: 3504

There are several appraoches for this. Although what you are doing is really not appropriate.

  1. AsyncTask

    • The thread pooling here is done internally,so you do not need to bother with that
    • Its a more cleaner approach to your problem instead of spawning individual threads.
    • If your user changes the screen during your API call ,you can also cancel the call.
    • You would have to enable notifyDatasetChanged()
    • You need to override very few functions to achieve the functionality that you want.
  2. AsyncTaskLoader

    • It gives you more control but you lose out on several implicitly defined functions
    • You need more knowledge to use this and should be well versed with classes like LoaderManager,Loader.
    • change is self trigerring Say if you were to change your underlying dataset,the changes would automatically trigger and provide a change to your UI.
  3. Handlers and Threads

    • This is one stpe above your current appraoch but provide way more benifits
    • You can abstract the thread creation and provide a handler which would handle the call for all your ids.
    • You could queue the threads and the messages delivered.
    • if the screen changes,you could remove callbacks and messages.

In conclusion,the major drawbacks of your current approach: - is loss of context when the changes need to be made. - the explicit creation of multiple threads

While the latter is a major issue,the more "user-noticeable" problem would the first.

There are and could be several other approaches based on the control you require and the your expertise with android callbacks and thread management.But these three are(according to me) most suitable.

PS:the common point in all these approaches is,

public View getView(int position, View convertView, ViewGroup     parent) { 
    ViewHolder holder;            
    if (convertView == null) {                
        //...         
    } 
    else {                
        //...
    }     

    //execute a task for your given id where task could be:
    //1. AsyncTask
    //2. AsyncTaskLoader
    //3. Handlers and thread
    //call notifyDataSetChanged() in all the cases,




    return convertView;
}

 @Override
 public void notifyDataSetChanged() {
        super.notifyDataSetChanged();
        //do any tasks which you feel are required
 } 

PPS:You could also look into DataSetObserver to again automate your requirements.

Upvotes: 2

Alex K
Alex K

Reputation: 8338

This is absolutely not a good way to go about updating information in a ListView. The getView method should simply create the view from data that you already have. It certainly shouldn't be running anything to get more information.

The best advice that I could give you is to fetch the data beforehand. Pull the data, update the ArrayList that your Adapter is connected to, then call adapter.notifyDataSetChanged(). This will redraw all of your information.

Pull the data all at once - not in small parts. This is the best and most reasonable way to do this.

Upvotes: 5

Related Questions