Android loaders, the way to go?

2019-03-08 08:31发布

问题:

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

回答1:

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.



回答2:

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.