domingo, 23 de diciembre de 2012

Pull to refresh en Android. Cargar los datos en un ListView.

URL ORIGEN: http://www.recursiveawesome.com/blog/2011/04/29/implementing-pull-to-refresh-in-your-android-app/


From the android-pulltorefresh repo, https://github.com/johannilsson/android-pulltorefresh
“Pull To Refresh” is a UI gesture made popular by the Twitter for Iphone app. It allows a user to refresh a list by pulling down and releasing from the top of the list. IOS developers have had library support for this feature for some time now. Over in Android-land, only the official Twitter app has managed to implement this gesture. Long-promised to be open-sourced, Twitter’s solution is still secret and Android developers have no official means to implement this increasingly demanded feature.
But thanks to Android’s open eco-system, we can build this into our Android app’s using the open-sourceandroid-pulltorefresh library developed by Johan Nilson. With thanks to the projects other contributors, his library is capable of producing a UI experience similar to the Twitter app, with support all the way back to 1.5.
The project provides a simple and sufficient example app to demonstrate the library use, but I needed something a bit more robust when I was implementing this in my application. So I thought I’d share my changes here, applying it to example application provided in the project.
Let’s start with a brief explanation of how this works. The android-pulltorefresh library provides a custom ListView that includes a special header. The list is initially displayed with the first item selected. This hides the header from view. The custom ListView responds to touch events so that when you pull down on the list, the header is displayed and handles displaying “pull” and “release” text and animation based on how far you are pulling down. Once its released, the header remains visible with the “loading” message until the view is told the refresh is complete, then it resets the header so its hidden once again.
Getting the basic functionality implemented is straight-forward. First, setup your layout and reference the custom ListView.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <!--
    The PullToRefreshListView replaces a standard ListView widget.
    -->
    <com.markupartist.android.widget.PullToRefreshListView
        android:id="@+id/android:list"
        android:layout_height="fill_parent"
        android:layout_width="fill_parent"
        />
</LinearLayout>
Now in your ListActivity you need to do 2 things, respond to the refresh request and let the view know when you’re done.
To handle the refresh action the custom view notifies an OnRefreshListener that you can implement to handle your refresh operation.
In the code below you’ll see that it implements the onRefresh method of the listener and wisely executes the refresh on a background thread using an AsyncTask.
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.pull_to_refresh);
 
        // Set a listener to be invoked when the list should be refreshed.
        ((PullToRefreshListView) getListView()).setOnRefreshListener(new OnRefreshListener() {
            @Override
            public void onRefresh() {
                // Do work to refresh the list here.
                new GetDataTask().execute();
            }
        });
        ....
Once your refresh is complete, notify the view by calling its onRefreshComplete() method. In this example, we call this method on the UI thread once the task is completed.
private class GetDataTask extends AsyncTask {
 
        @Override
        protected Void doInBackground(Void... params) {
            // Simulates a background job.
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                ;
            }
            return null;
        }
 
        @Override
        protected void onPostExecute() {
            mListItems.addFirst("Added after refresh...");
 
            // Call onRefreshComplete when the list has been refreshed.
            ((PullToRefreshListView) getListView()).onRefreshComplete();
 
            super.onPostExecute();
        }
    }
So that’s basically how the example app from the project is setup. It just initializes a list of Strings and then on every refresh adds a String to the beginning of the list. In a real application we’ll probably need to beef this up a bit.
First let’s take that Task to task and do some real work. In my situation I need to refresh the list by calling an external api and retrieving all the data to be displayed. In this case its actually more efficient just to rebuild the whole list rather than calculating deltas and prepending to the existing list. For this example, assume we have a List as a class-level field managed by a custom ArrayAdapter.
Here’s the first iteration, the list is re-initialized in the task’s onPreExecute() phase, and the work is moved into the doInBackground() method.
private class GetDataTask extends AsyncTask {
 
        @Override
        protected void onPreExecute() {
		mList = new ArrayList();
	}    
 
        @Override
        protected Void doInBackground(Void... params) {
            JSONArray results = api.getData();
            for (int i=0; i &lt; results.length(); i++) {
                ListData data = parseResult(results[i]);
                mList.add(data);
            }
            return null;
        }
 
        @Override
        protected void onPostExecute() {
            // Call onRefreshComplete when the list has been refreshed.
            ((PullToRefreshListView) getListView()).onRefreshComplete();
 
            super.onPostExecute();
        }
    }
Seems like that should work ok, but I ran into a problem with this approach. As the user pulls down on the list, the onRefresh() method is called triggering the task, which starts by clearing out the mList array. At the same time, it seems that because of the UI changes, the system appears to be taking some measurements which is causing the getView() method to be called on the adapter that is managing the list. Now because we’ve emptied out the list, the application can exhibit some unpredictable behavior.
Let’s solve this in the next iteration by loading a temporary list in the background, and switching the list once we’re done. We should also consider that its possible we have some error while retrieving the new data, so in that case let’s not refresh the existing list. At least the user will still see their existing data.
Ok, now the GetDataTask looks like this:
private class GetDataTask extends AsyncTask {
 
        private List localList;
 
        @Override
        protected void onPreExecute() {
	    localList = new ArrayList();
        }    
 
        @Override
        protected Void doInBackground(Void... params) {
 
            try{
                JSONArray results = api.getData();
                for (int i=0; i &lt; results.length(); i++) {
                    ListData data = parseResult(results[i]);
                    localList.add(data);
                }
            } catch (Exception ex){
                localList=null;
                Log.e(TAG,&quot;Exception during refresh:&quot;,ex);
            }
            return null;
        }
 
        @Override
        protected void onPostExecute() {
 
             if(localList != null &amp;&amp; !localList.empty()){
                 mList = localList;
                 mListAdapter.notifyDataSetChanged();
             }
            // Call onRefreshComplete when the list has been refreshed.
            ((PullToRefreshListView) getListView()).onRefreshComplete();
 
            super.onPostExecute();
        }
    }
Alright, that’s looking pretty good now. For completeness we really should handle the possibility of the task getting cancelled. This can happen when the user navigates away from the app and the task is killed before its completed. This will cause the onPostExecute() method to not be called and so theonRefreshComplete() method won’t be called. Depending on how the user navigates through the app, they could return to this activity without going through the complete onCreate() lifecycle, and you’ll end up with the screen still showing the “loading” progress message in the header. This is common when using tabs between multiple ListViews.
Also, the documented best practices for implementing an AsyncTask says that in long running background work you should periodically check if the task has been cancelled and try to gracefully quit your work and exit. So let’s get all of that in there.
private class GetDataTask extends AsyncTask {
 
        private List localList;
 
        @Override
        protected void onPreExecute() {
	    localList = new ArrayList();
        }    
 
        @Override
        protected Void doInBackground(Void... params) {
 
            try{
                JSONArray results = api.getData();
 
                // check if task was cancelled during long api call
                if(isCancelled(){
                  return null;
               }
                for (int i=0; i &lt; results.length(); i++) {
                    ListData data = parseResult(results[i]);
                    localList.add(data);
                }
            } catch (Exception ex){
                localList=null;
                Log.e(TAG,&quot;Exception during refresh:&quot;,ex);
            }
            return null;
        }
 
        @Override
        protected void onPostExecute() {
 
             if(localList != null &amp;&amp; !localList.empty()){
                 mList = localList;
                 mListAdapter.notifyDataSetChanged();
             }
            // Call onRefreshComplete when the list has been refreshed.
            ((PullToRefreshListView) getListView()).onRefreshComplete();
 
            super.onPostExecute();
        }
 
        @Override
        protected void onCancelled() {
             // reset the UI
             ((PullToRefreshListView) getListView()).onRefreshComplete();
        }
    }
Now that you have a solid refresh implementation you may want to tweak the design of the refresh header. In the sample application they are using the Android “Light” theme. This makes the text black and easy to read. But if you’re using the default “Dark” theme you’ll find the text very hard to see. Here’s a look at the text portion of the refresh header layout, you can see that it references an attribute for the textAppearance style.
        <TextView
            android:id="@+id/pull_to_refresh_text"
            android:text="@string/pull_to_refresh_tap_label"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textStyle="bold"
            android:paddingTop="5dip"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center"
        />
Let’s tweak the color of the text to make it readable in our application. To do that, we’ll create a theme that overrides the style used by the textAppearanceMedium attribute. We’ll override this with our own style, which will inherit from the Android TextAppearance.Medium style and override the textColor attribute. Got all that?
First, here’s what Android’s style declaration looks like:
<style name="TextAppearance.Medium">
        <item name="android:textSize">18sp</item>
        <item name="android:textStyle">normal</item>
        <item name="android:textColor">?textColorPrimary</item>
</style>
Now here’s our theme and style declaration:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="MyCustomTheme" parent="android:Theme.NoTitleBar.Fullscreen">
    	<item name="android:textAppearanceMedium">@style/TextAppearance.Medium</item>
    </style>
 
    <style name="TextAppearance.Medium" parent="android:TextAppearance.Medium">
    	<item name="android:textColor">#FFCDC9C9</item>
    </style>
</resources>
And finally edit the Android Manifest in order to use the new theme:
<application android:name="MyApplication"
                     android:icon="@drawable/icon"
                     android:label="@string/app_name"
                     android:theme="@style/MyCustomTheme">
</application>
And that’s it, everything you you need to implement the pull to refresh gesture, handle the operation and customize the UI.
By the way, the android-pulltorefresh library is made available under the Apache License, V2.0. So you need to maintain any NOTICE files and copyright headers from the source. But you are free to redistribute in derivative works for commercial use.
Originally posted on Shared State

No hay comentarios:

Publicar un comentario