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
In case someone is looking for the loader version of my previous example : here it is :
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.
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.