6

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.

Community
  • 1
  • 1
Lisa Anne
  • 4,482
  • 17
  • 83
  • 157
  • 1
    main problem: WT* is `CheckSomeInfoOverTheInternet` isin't it async already? ... normal way is: create handler on UI thread(lets call it UIHandler) ... create HandlerThread(HT) ... post long running stuff to HT with some callback ... if HT take care of its job call some callback using UIHandler ... end of story .. – Selvin Feb 13 '15 at 16:47
  • From your question above you make it sound like even if you perform an asynchronous operation in your `getView` you will still have to wait for the results since you are using said results to update your view. Is this the case? I think a better approach would be to perform your asynchronous operation before you update your view and then simply pass the results to `getView`. This way you are not pending on an operation from within `getView` – Willis Feb 13 '15 at 16:52
  • 1
    ... of course only one UIHandler and HandlerThread per adapter - not per getView call ... also this is almost the same thing which is under the AsyncTask (the new one with executor) ... and runOnUiThread is nothing but posting the runnable to UI handler – Selvin Feb 13 '15 at 16:54
  • 1
    @Willis Hi Wills, I concur, prefetching could be a good idea, but the problem remains, how to perform the prefetching (which is asynchronous)? A handler as Selvin suggests? An Executor? Many threads like I do... – Lisa Anne Feb 13 '15 at 16:57
  • @Lisa Anne - there are a lot of options, it just depends what you all need to do. `AsyncTask` is one possibility. It even provides a mechanism for updating your UI directly from the task via the `onPostExecute` method which itself runs on the UIThread – Willis Feb 13 '15 at 17:02
  • 1
    ...Remember about convertView... It can kick you in the as*... When you scroll the ListView and getView is called you are starting async operation... Now, what will happen when async operation will ends after convertView is used by ListView again? You will end with wrong data... – Selvin Feb 13 '15 at 17:12
  • @Selvin please Selvin, correct me if I am wrong: your approach is not radically different form mine: at the end of the day you start tasks as I do and as many as I do, we do not track or cancel them... am I right? – Lisa Anne Feb 13 '15 at 17:13
  • 1
    Creating a thread which contains a Runnable which runs another runnable on the UI thread is bad practice by any standard. How did you arrive there? – 323go Feb 13 '15 at 17:14
  • @323go I agree on that :-) – Lisa Anne Feb 13 '15 at 17:15
  • @Selvin oh yesss! Recycling is bad here! I agree – Lisa Anne Feb 13 '15 at 17:18
  • Why don't you have the necessary data to render your views already? Instead of fetching it 1 by 1 get pages of 10 or 20 or whatever makes sense. You should have the data before you attempt to render the items. – darnmason Feb 13 '15 at 17:46
  • @darnmason I agree darnmason, but I think that pre-fetching is only part of the story, because I might not be able to pre-fetch everything – Lisa Anne Feb 13 '15 at 18:37
  • I think the problem is where you are doing the asynchronous call. Do it outside the adapter, then populate the adapter as you get information. – James Black Feb 22 '15 at 20:47

7 Answers7

5

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.

Alex K
  • 8,269
  • 9
  • 39
  • 57
  • 1
    That does make life simple, but it's only practically workable for smaller data sets. In situations where the user can navigate through more data than you can reasonably pre-cache, some form of on-demand loading will be required. – Chris Stratton Feb 21 '15 at 06:10
  • So would `RecyclerView` help in such cases?Or maybe a `AsyncLoader`? – Droidekas Feb 22 '15 at 07:45
4

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();
    }
      }
    
denvercoder9
  • 2,979
  • 3
  • 28
  • 41
  • Thanks Rafiduzzaman! Please see my edits: the problem is more general: the issue is how to update the view created in `getView()` depending on the result of the asynchronous callback – Lisa Anne Feb 23 '15 at 08:16
  • Why is it so hard? It really depends on what views you have and what views you want to update. You have got the information downloaded, parsed it to an arraylist of objects and passed to the adapter like you would do with a normal listview. Now you just update the views in getView(). I suggest you study this http://www.vogella.com/tutorials/AndroidListView/article.html – denvercoder9 Feb 23 '15 at 10:09
2

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.

Droidekas
  • 3,464
  • 2
  • 26
  • 40
1

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/

Vamsi
  • 878
  • 1
  • 8
  • 25
1

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

jzyamateur
  • 113
  • 9
  • Thanks jzy, please see my edit: the whole point is not how to execute the network requests in `getView` but, rather, how to updated the view created depending on the result of the asynchronous callback – Lisa Anne Feb 23 '15 at 08:03
1

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.

Yuriy Kulikov
  • 2,059
  • 1
  • 16
  • 29
  • In the [tutorial](http://developer.android.com/training/displaying-bitmaps/display-bitmap.html) on the android developers website, they load the images asynchronously in `getView()`. They also show you how to avoid problems if your (image)view is not on screen anymore when your task finishes. – Jonas Czech Feb 22 '15 at 13:12
  • Loading images asynchronously is very different from loading something from the web. Loading images will always succeed, and it does not take longer time. Besides, it is likely that these image will be only used by this particular views. If you retrieve something from web, you may have to implement complicated retry mechanisms, save results for future use or something else. This will make it a lot more complicated. – Yuriy Kulikov Feb 22 '15 at 15:30
  • You were not specifically talking about loading stuff from the web :-) . If you want to load images asynchronously from the web, there are various libraries to do it. (Picasso , Volley...) If you want to load other stuff from the web, there are libraries for that too, which make it very easy. – Jonas Czech Feb 23 '15 at 11:13
1

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

C B J
  • 1,838
  • 16
  • 22