26

I am used to building lists in android using adapters. If I need some long-to-get data, I use an asynctask, or a simple runnable, to update the data structure on which the adapter rely, and call notifyDataChanged on the adapter.

Although it is not straightforward, I finally find this is a simple model and it allows a good separation of logic presentation (in the asynctask, update a data structure) and the view (an adapter acting as a view factory, mostly).

Nevertheless, I read recently about loaders introduced in HoneyComb and included in the backward compatibility support-library, I tried them and find the introduce a lot of complexity. They are difficult to handle and add some kind of magic to this whole process through loader managers, add a lot of code and don't decrease the number of classes or collaborating items but I may be wrong and would like to hear some good points on loaders.

  • What are they advantages of loaders in terms of lines of code, clarity and effort ?
  • What are they advantages of loaders in terms of role separation during data loading, or more broadly, in terms of design ?
  • Are they the way to go, should I replace all my list data loading to implement them through loaders ?

Ok, this is a developers' forum, so here is an example. Please, make it better with loaders :

package com.sof.test.loader;

import java.util.ArrayList;
import java.util.List;

import android.app.ListActivity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ArrayAdapter;
import android.widget.TextView;

/** The activity. */
public class LoaderTestActivity extends ListActivity {

    private DataSourceOrDomainModel dataSourceOrDomainModel = new DataSourceOrDomainModel();
    private List<Person> listPerson;
    private PersonListAdapter personListAdapter;
    private TextView emptyView;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        listPerson = new ArrayList<Person>();
        personListAdapter = new PersonListAdapter( listPerson );
        setListAdapter( personListAdapter );
        setUpEmptyView();
        new PersonLoaderThread().execute();
    }

    public void setUpEmptyView() {
        emptyView = new TextView( this );
        emptyView.setLayoutParams( new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT ) );
        emptyView.setVisibility(View.GONE);
         ((ViewGroup)getListView().getParent()).addView(emptyView);
        getListView().setEmptyView(emptyView);
    }

    /** Simulate a long task to get data. */
    private class PersonLoaderThread extends AsyncTask<Void, Integer, List<Person>> {
        @Override
        protected List<Person> doInBackground(Void... params) {
            return dataSourceOrDomainModel.getListPerson( new ProgressHandler());
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            emptyView.setText( "Loading data :" + String.valueOf( values[ 0 ] ) +" %" );
        }

        @Override
        protected void onPostExecute(List<Person> result) {
            listPerson.clear();
            listPerson.addAll( result );
            personListAdapter.notifyDataSetChanged();
        }

        private class ProgressHandler implements ProgressListener {

            @Override
            public void personLoaded(int count, int total) {
                publishProgress( 100*count / total );
            }

        }
    }

    /** List item view factory : the adapter. */
    private class PersonListAdapter extends ArrayAdapter<Person> {

        public PersonListAdapter( List<Person> listPerson ) {
            super(LoaderTestActivity.this, 0, listPerson );
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if( convertView == null ) {
                convertView = new PersonView( getContext() );
            }
            PersonView personView = (PersonView) convertView;
            personView.setPerson( (Person) getItem(position) );
            return personView;
        }
    }
}

A small callback interface for progress

package com.sof.test.loader;

/** Callback handler during data load progress. */
public interface ProgressListener {
    public void personLoaded(int count, int total );
}

A list item widget

package com.sof.test.loader;

import com.sof.test.loader.R;
import android.content.Context;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import android.widget.TextView;

/** List Item View, display a person */
public class PersonView extends LinearLayout {

    private TextView personNameView;
    private TextView personFirstNameView;

    public PersonView(Context context) {
        super(context);
        LayoutInflater inflater= (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate( R.layout.person_view,this );
        personNameView = (TextView) findViewById( R.id.person_name );
        personFirstNameView = (TextView) findViewById( R.id.person_firstname );
    }

    public void setPerson( Person person ) {
      personNameView.setText( person.getName() );   
      personFirstNameView.setText( person.getFirstName() );
    }
}

It's xml : res/person_view.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/person_view"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >

    <TextView
        android:id="@+id/person_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true" />

    <TextView
        android:id="@+id/person_firstname"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/person_name" />

</RelativeLayout>

The data source or model, providing data (slowly)

package com.sof.test.loader;

import java.util.ArrayList;
import java.util.List;

/** A source of data, can be a database, a WEB service or a model. */
public class DataSourceOrDomainModel {
    private static final int PERSON_COUNT = 100;

    public List<Person> getListPerson( ProgressListener listener ) {
        List<Person> listPerson = new ArrayList<Person>();
        for( int i=0; i < PERSON_COUNT ; i ++ ) {
            listPerson.add( new Person( "person", "" + i ) );
            //kids, never do that at home !
            pause();
            if( listener != null ) {
                listener.personLoaded(i,PERSON_COUNT);
            }//if
        }
        return listPerson;
    }//met

    private void pause() {
        try {
            Thread.sleep( 100 );
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

The POJO representing a person :

package com.sof.test.loader;

/** A simple POJO to be displayed in a list, can be manipualted as a domain object. */
public class Person {
    private String name;
    private String firstName;

    public Person(String name, String firstName) {
        this.name = name;
        this.firstName = firstName;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
}//class
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Snicolas
  • 37,840
  • 15
  • 114
  • 173
  • 11
    Nobody is holding a gun to your head to use loaders. Or, if they are, please cough twice in a response, and we'll try to get you some help. – CommonsWare May 02 '12 at 22:52
  • 7
    I guess I tried to ask and you tried to answer. – Snicolas May 02 '12 at 23:00
  • p.s. I know you have 6.4k reputation and everything... but sentences like "Please make [this code] better" don't really follow the "question-answer" format. – Alex Lockwood May 02 '12 at 23:06
  • 2
    I was not meant to offend in any case, I am not a native english speaker. This was maybe an unskilful way to invite people to enhance the code base I gave. – Snicolas May 02 '12 at 23:10
  • That's what I figured... I assumed you didn't mean it to come across that way :P. – Alex Lockwood May 02 '12 at 23:32

2 Answers2

15

In case someone is looking for the loader version of my previous example : here it is :

package com.sof.test.loader;

import java.util.ArrayList;
import android.app.LoaderManager;
import java.util.List;

import android.app.ListActivity;
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.content.Loader;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ArrayAdapter;
import android.widget.TextView;

/** The activity. */
public class LoaderTestActivity2 extends ListActivity implements
        LoaderManager.LoaderCallbacks<List<Person>> {

    private DataSourceOrDomainModel dataSourceOrDomainModel = new DataSourceOrDomainModel();
    private List<Person> listPerson;
    private PersonListAdapter personListAdapter;
    private TextView emptyView;
    private Loader<List<Person>> personLoader;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        listPerson = new ArrayList<Person>();
        personListAdapter = new PersonListAdapter(listPerson);
        setListAdapter(personListAdapter);
        personLoader = new PersonLoader(this, dataSourceOrDomainModel, new ProgressHandler() );
        setUpEmptyView();
        getLoaderManager().initLoader(0, null, this);
        personLoader.forceLoad();
    }

    public void setUpEmptyView() {
        emptyView = new TextView(this);
        emptyView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT));
        emptyView.setVisibility(View.GONE);
        ((ViewGroup) getListView().getParent()).addView(emptyView);
        getListView().setEmptyView(emptyView);
    }

    public void publishProgress(int progress) {
        emptyView.setText("Loading data :" + String.valueOf(progress) + " %");
    }

    @Override
    public Loader<List<Person>> onCreateLoader(int arg0, Bundle arg1) {
        return personLoader;
    }

    @Override
    public void onLoadFinished(Loader<List<Person>> personLoader, List<Person> result) {
        listPerson.clear();
        listPerson.addAll(result);
        personListAdapter.notifyDataSetChanged();
    }

    @Override
    public void onLoaderReset(Loader<List<Person>> arg0) {
        listPerson.clear();
        personListAdapter.notifyDataSetChanged();
    }

    /** List item view factory : the adapter. */
    private class PersonListAdapter extends ArrayAdapter<Person> {

        public PersonListAdapter(List<Person> listPerson) {
            super(LoaderTestActivity2.this, 0, listPerson);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if (convertView == null) {
                convertView = new PersonView(getContext());
            }
            PersonView personView = (PersonView) convertView;
            personView.setPerson((Person) getItem(position));
            return personView;
        }
    }

    private class ProgressHandler implements ProgressListener {

        @Override
        public void personLoaded(final int count, final int total) {
            runOnUiThread( new Runnable() {
                @Override
                public void run() {
                    publishProgress(100 * count / total);                   
                }
            });
        }
    }
}

 class PersonLoader extends AsyncTaskLoader<List<Person>> {

    private DataSourceOrDomainModel dataSourceOrDomainModel;
    private ProgressListener progressHandler;

    public PersonLoader(Context context, DataSourceOrDomainModel dataSourceOrDomainModel, ProgressListener progressHandler ) {
        super(context);
        this.dataSourceOrDomainModel = dataSourceOrDomainModel;
        this.progressHandler = progressHandler;
    }

    @Override
    public List<Person> loadInBackground() {
        return dataSourceOrDomainModel.getListPerson( progressHandler );
    }
}

It would be more difficult to add support (support librairy) to this example as there is no equivalent of ListAcitivity in the support librairy. I would have either to create a ListFragment or create an FragmentActivity and give it a layout including a list.

Snicolas
  • 37,840
  • 15
  • 114
  • 173
11

One problem your code has which loaders aim to fix is what happens if your activity is restarted (say due to device rotation or config change) while your async task is still in progress? in your case your restarted activity will start a 2nd instance of the task and throw away the results from the first one. When the first one completes you can end up with crashes due to the fact your async task has a reference is what is now a finished activity.

And yes using loaders often makes for more/more complex code, particularly if you can't use one of the provided loaders.

superfell
  • 18,780
  • 4
  • 59
  • 81
  • Loader are a bit more difficult to handle, you are right. They don't have a very interesting mechanism to display background operation progress neither and lead to more code. But, as you said, they provide a support for configChanges and data source changes. – Snicolas May 03 '12 at 07:06
  • 8
    By the way there is an error in the Loader Framework. If you receive the error "object returned by onCreate must be a non-static inner class", the message is wrong : it must be an instanceof either a separate real class or a static inner class. – Snicolas May 03 '12 at 07:08
  • Other good complement here : http://stackoverflow.com/questions/5097565/can-honeycomb-loaders-solve-problems-with-asynctask-ui-update – Snicolas May 03 '12 at 13:27