今更ながら、ORMLiteを使ってアドレス帳アプリを作ってみた

Androidで使えるORMライブラリについて調べて、ORMLiteを使ってアドレス帳アプリをサンプルとして作ってみました。ORMLiteを使うのは今更感はありますが。

ORMライブラリについては、@pside氏の 天下一「AndroidのORM」武道会 の記事が詳しいです。主要なライブラリは、次の通りで、私が調べたFull text search 3(FTS3)のサポート状況を付け加えます。

  • ORMLite: 直接なサポートはないが、raw queyインターフェエースを使うといいという話。 FTS3 searches in ORMLite?
  • ActiveAndroid: full text searchはサポートされているようだ。 Add FTS support #272
  • greenDAO: サポートされていない。
  • Ollie: 調査せず
  • SugarORM: サポートの有無不明。
  • DBFlow: サポートの有無不明。
  • Realm: サポートせず。中の人によると、Coreでタスクリストにあるとのこと。
  • couchbase: サポートせず。

上で引用した @pside 氏の記事によると、NoSQLのRealmは極端に処理が速いとのことですが、FTS3をサポートしていません。SQLiteを使っているGreenDaoは、SQLiteを使っているORMの中で一番処理が速いとのことですが、これもFTS3をサポートしていません。私が調べた限りでは、FTS3が使えるのは、ORMLiteとActiveRecordですが、@pside氏の記事では、ORMLiteの方が若干性能がいいようなので、今回は、ORMLiteを使いました。

今回は、ORMLiteを使ってアドレス帳のアプリをサンプルとして作りました。上で、FTS3について言及していますが、今回は、FTS3の機能は使わず、データベースのテーブルに関連がある場合のアプリの実装の練習をしました。題材は、アドレス帳で、テーブルは、2つあります。1つめのテーブルは、personというテーブルで、カラムにid、name、addressを持ちます。もう1つのテーブルは、addressというテーブルで、カラムに、id、zipcode、prefecture、city、otherを持ちます。2つのテーブルは、1対1の関係にあります。

まず、Intellijで、Gradle: Android Moduleで新規プロジェクトを作り、app/build.gradleのdependenciesに

compile 'com.j256.ormlite:ormlite-android:4.48'

を追加します。

エンティティクラスは、

Person.java

@DatabaseTable(tableName = "person")
public class Person {

    @DatabaseField(generatedId = true)
    private Integer id;
    @DatabaseField
    private String name;
    @DatabaseField(foreign = true, foreignAutoCreate = true, foreignAutoRefresh = true)
    private Address address;

    public Person() {
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

Address.java

@DatabaseTable(tableName = "address")
public class Address {

    @DatabaseField(generatedId = true)
    private Integer id;
    @DatabaseField
    private String zipcode;
    @DatabaseField
    private String prefecture;
    @DatabaseField
    private String city;
    @DatabaseField
    private String other;


    public Address() {
    }

    public Integer getId() {
        return id;
    }

    public String getZipcode() {
        return zipcode;
    }

    public void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }

    public String getPrefecture() {
        return prefecture;
    }

    public void setPrefecture(String prefecture) {
        this.prefecture = prefecture;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getOther() {
        return other;
    }

    public void setOther(String other) {
        this.other = other;
    }
}

です。フィールドには、@DatabaseFieldアノーテーションを付け、自動生成する主キーには、generatedId = trueを付けます。また、Personは、Addressを持っているので、Personクラスで、Address型のフィールドが、関連する永続化フィールドであることをしめすために、foregin = trueを付けます。あと、foreignAutoCreate = trueは、関連に設定されたエンティティを自動で生成するために、foreignAutoRefresh = trueは、クエリが発行された際に自動で更新するために付けます。

次に、com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper を継承したクラスを作ります。

DatabaseHelper.java

public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
    private final static String TAG = DatabaseHelper.class.getSimpleName();

    private final static String DATABASE_NAME = "addressbook.db";
    private final static int DATABASE_VERSION = 1;

    private Dao<Person, Integer> mPersonDao;
    private Dao<Address, Integer> mAddressDao;

    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
        try {
            // エンティティを指定してcreate tableする
            TableUtils.createTable(connectionSource, Person.class);
            TableUtils.createTable(connectionSource, Address.class);
        } catch (SQLException e) {
            Log.e(TAG, "データベースを作成できませんでした", e);
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion) {
        // 省略
    }

    public Dao<Person, Integer> getPersonDao() throws SQLException {
        if (mPersonDao == null) {
            mPersonDao = getDao(Person.class);
        }
        return mPersonDao;
    }

    public Dao<Address, Integer> getAddressDao() throws SQLException {
        if (mAddressDao == null) {
            mAddressDao = getDao(Address.class);
        }
        return mAddressDao;
    }
}

onCreateメソッドとonUpgradeメソッドをオーバーライドするのは、SQLiteの場合と同じですが、onCreateメソッドでは、TableUtilsクラスを使ってテーブルを生成します。また、ORMLiteでは、Daoクラスの生成はコストがかかるということで、それぞれ、PersonのDaoとAddressのDaoオブジェクトがないときに限り、getPersonDaoとgetAddressDaoメソッドは、getDaoメソッドでDaoオブジェクトを作り値を返し、Daoオブジェクトがある場合は、それを返します。

そして、各アクティビティで、Daoメソッドを使うため、Applicationクラスを継承したクラスで、DatabaseHelperオブジェクトを返すメソッドを定義します。

AddressApplication.java

public class AddressApplication extends Application {
    private static AddressApplication instance;
    private DatabaseHelper mDatabaseHelper;

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
    }

    public static AddressApplication getInstance() {
        return instance;
    }

    public DatabaseHelper getDatabaseHelper() {
        if (mDatabaseHelper == null) {
            mDatabaseHelper = new DatabaseHelper(this);
        }
        return mDatabaseHelper;
    }
}

最後に、アプリのメイン画面は、名前リストで、アクティビティは、

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private final static String TAG = MainActivity.class.getSimpleName();

    private List<String > mList = new ArrayList<String>();
    private ListView mListView;
    private DatabaseHelper mHelper;

    public final static String TYPE = "type";
    public final static String CREATE_DATA_TYPE = "create";
    public final static String UPDATE_DATA_TYPE = "update";


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHelper = AddressApplication.getInstance().getDatabaseHelper();

        mListView = (ListView)findViewById(android.R.id.list);

        mList = getAllList();

        ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(this, R.layout.rowdata, mList);
        mListView.setAdapter(arrayAdapter);

        mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                ListView listView = (ListView)parent;
                String name = (String)listView.getItemAtPosition(position);
                Intent intent = new Intent(MainActivity.this, RegisterActivity.class);
                intent.putExtra(TYPE, UPDATE_DATA_TYPE);
                intent.putExtra("name", name);
                startActivity(intent);
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);

        MenuItem item = menu.add("NEW");
        item.setIcon(android.R.drawable.ic_input_add);
        item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem menuItem) {
                Intent intent = new Intent(MainActivity.this, RegisterActivity.class);
                intent.putExtra(TYPE, CREATE_DATA_TYPE);
                startActivity(intent);

                return false;
            }
        });
        MenuItemCompat.setShowAsAction(item,
                MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    public List<String> getAllList() {
        List<String> list  = new ArrayList<String>();
        try {
            List<Person> persons =  mHelper.getPersonDao().queryForAll();
            for (Person person: persons) {
                list.add(person.getName());
            }
            return list;
        } catch (SQLException e) {
            Log.e(TAG, "例外が発生しました", e);
            return null;
        }
    }

    @Override
    protected void onRestart() {
        mListView = (ListView)findViewById(android.R.id.list);

        mList = getAllList();

        ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(this, R.layout.rowdata, mList);
        mListView.setAdapter(arrayAdapter);

        super.onRestart();
    }
}

リストは、名前のリストなので、

public List<String> getAllList() {
        List<String> list  = new ArrayList<String>();
        try {
            List<Person> persons =  mHelper.getPersonDao().queryForAll();
            for (Person person: persons) {
                list.add(person.getName());
            }
            return list;
        } catch (SQLException e) {
            Log.e(TAG, "例外が発生しました", e);
            return null;
        }
    }

のように、mHelperからPersonのDaoを得て、全件取得 queryForAllを実行しています。

メイン画面のActionBarのメニューのプラスアイコンをタップすると、名前、アドレス入力画面に遷移し、リストに名前がある時、リストの行をタップすると、アドレス更新画面に遷移します。遷移先のアクティビティのクラスは、次のように定義されています。

RegisterActivity.java

public class RegisterActivity extends AppCompatActivity
        implements View.OnClickListener {
    private final static String TAG = RegisterActivity.class.getSimpleName();

    private DatabaseHelper mHelper;
    private Button mBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_register);

        mHelper = AddressApplication.getInstance().getDatabaseHelper();

        mBtn = (Button)findViewById(R.id.btn);
        mBtn.setOnClickListener(this);

        String type = getIntent().getStringExtra(MainActivity.TYPE);

        Resources resources = getResources();
        if (type.equals(MainActivity.CREATE_DATA_TYPE)) {
            mBtn.setText(resources.getString(R.string.register));
        } else if (type.equals(MainActivity.UPDATE_DATA_TYPE)) {
            mBtn.setText(resources.getString(R.string.update));
            showAddress();
        }
    }

    @Override
    public void onClick(View view) {
        int id = view.getId();
        if (id == R.id.btn) {
            String type = getIntent().getStringExtra(MainActivity.TYPE);
            if (type.equals(MainActivity.CREATE_DATA_TYPE)) {
                registerAddressBook();
            } else if (type.equals(MainActivity.UPDATE_DATA_TYPE)) {
                updateAddressBook();
            }
        }
    }

    public void registerAddressBook() {
        String name = ((EditText)findViewById(R.id.nameEt)).getText().toString();
        String zipcode = ((EditText)findViewById(R.id.zipcodeEt)).getText().toString();
        String prefecture = ((EditText)findViewById(R.id.prefectureEt)).getText().toString();
        String city = ((EditText)findViewById(R.id.cityEt)).getText().toString();
        String other = ((EditText)findViewById(R.id.otherEt)).getText().toString();

        if (!checkDatum(name, zipcode, prefecture, city, other)) {
            return;
        }

        if (isExists(name)) {
            Toast.makeText(this, name + " has been registered already or some error occurs.", Toast.LENGTH_LONG).show();
            return;
        }

        Address address = new Address();
        address.setZipcode(zipcode);
        address.setPrefecture(prefecture);
        address.setCity(city);
        address.setOther(other);
        Person person = new Person();
        person.setName(name);
        person.setAddress(address);
        try {
            mHelper.getPersonDao().create(person);
            finish();
        } catch (SQLException e) {
            Toast.makeText(this, "cannot register address.", Toast.LENGTH_LONG).show();
            Log.e(TAG, e.getMessage());
        }
    }

    public void updateAddressBook() {
        String name = ((EditText)findViewById(R.id.nameEt)).getText().toString();
        String zipcode = ((EditText)findViewById(R.id.zipcodeEt)).getText().toString();
        String prefecture = ((EditText)findViewById(R.id.prefectureEt)).getText().toString();
        String city = ((EditText)findViewById(R.id.cityEt)).getText().toString();
        String other = ((EditText)findViewById(R.id.otherEt)).getText().toString();

        if (!checkDatum(name, zipcode, prefecture, city, other)) {
            return;
        }

        try {
            Person person = mHelper.getPersonDao().queryForEq("name", name).get(0);
            Address address = person.getAddress();
            address.setZipcode(zipcode);
            address.setPrefecture(prefecture);
            address.setCity(city);
            address.setOther(other);
            mHelper.getAddressDao().update(address);
            mHelper.getPersonDao().update(person);
            finish();
        } catch (SQLException e) {
            Log.e(TAG, e.getMessage());
        }
    }

    public void showAddress() {
        String type = getIntent().getStringExtra(MainActivity.TYPE);
        if (!type.equals(MainActivity.UPDATE_DATA_TYPE)) return;

        String name = getIntent().getStringExtra("name");

        List<Person> list = null;
        Person person;
        Address address;

        try {
            list = mHelper.getPersonDao().queryForEq("name", name);
            person = list.get(0);
        } catch (SQLException e) {
            // ありえない
            Log.e(TAG, e.getMessage());
            return;
        }

        address = person.getAddress();

        String zipcode = address.getZipcode();
        String prefecture = address.getPrefecture();
        String city = address.getCity();
        String other = address.getOther();

        ((EditText)findViewById(R.id.nameEt)).setText(name);
        ((EditText)findViewById(R.id.nameEt)).setEnabled(false);
        ((EditText)findViewById(R.id.zipcodeEt)).setText(zipcode);
        ((EditText)findViewById(R.id.prefectureEt)).setText(prefecture);
        ((EditText)findViewById(R.id.cityEt)).setText(city);
        ((EditText)findViewById(R.id.otherEt)).setText(other);
    }

    public boolean isExists(String name) {
        List<Person> list = null;
        try {
            list = mHelper.getPersonDao().queryForEq("name", name);
            if (list.isEmpty()) {
                return false;
            } else {
                return true;
            }
        } catch (SQLException e) {
            Log.e(TAG, e.getMessage());
            return false;
        }

    }

    public boolean checkDatum(String name, String zipcode, String prefecture,
                           String city, String other) {
        if (TextUtils.isEmpty(name)) {
            Toast.makeText(this, "name is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        if (TextUtils.isEmpty(zipcode)) {
            Toast.makeText(this, "zipcode is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        if (TextUtils.isEmpty(prefecture)) {
            Toast.makeText(this, "prefecture is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        if (TextUtils.isEmpty(city)) {
            Toast.makeText(this, "city is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        if (TextUtils.isEmpty(other)) {
            Toast.makeText(this, "other is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        return true;
    }
}

新規にアドレスを入力するケースでは、

        Address address = new Address();
        address.setZipcode(zipcode);
        address.setPrefecture(prefecture);
        address.setCity(city);
        address.setOther(other);
        Person person = new Person();
        person.setName(name);
        person.setAddress(address);
        try {
            mHelper.getPersonDao().create(person);
            finish();
        } catch (SQLException e) {
            Toast.makeText(this, "cannot register address.", Toast.LENGTH_LONG).show();
            Log.e(TAG, e.getMessage());
        }

のように、まず、Addressオブジェクトを作り、住所を入れ、次に、Personオブジェクトを作り、名前と住所を入れます。そして、mHeperのPersonに関するDaoオブジェクトを得て、createメソッドの引数にpersonを渡して、データ登録をします。

データ更新のケースでは、簡単のため、名前の変更はないものとして、次のようにデータ更新を行います。

        try {
            Person person = mHelper.getPersonDao().queryForEq("name", name).get(0);
            Address address = person.getAddress();
            address.setZipcode(zipcode);
            address.setPrefecture(prefecture);
            address.setCity(city);
            address.setOther(other);
            person.setAddress(address);
            mHelper.getAddressDao().update(address);
            mHelper.getPersonDao().update(person);
            finish();
        } catch (SQLException e) {
            Log.e(TAG, e.getMessage());
        }

名前の変更はないし、同じ名前を入力しようとすると、警告を表示してデータ登録をできない実装にしているため、メイン画面で名前をタップすると、当然データがあるので、mHelper.getPersonDao().queryForEq(“name”, name)は1個しか要素を含まないListを返すので、get(0)でPersonオブジェクトを得ます。次に、Personオブジェクトから、getAddressメソッドで住所を得ます。そして、更新のため入力された住所情報でAddressオブジェクトを更新シ、さらに、更新された住所でPersonオブジェクトを更新します。mHelper.getAddressDao().update(address)とmHelper.getPersonDao().update(person)でデータベースのデータを更新します。

作ったサンプルを

andropenguin/AddressBookORMlite

に公開します。

参考サイト
天下一「AndroidのORM」武道会
Ormlite for Android Using Ormlite for clean and efficient database managment
OrmLite調査メモ

リストを引っ張って更新するライブラリshontauro/android-pulltorefresh-and-loadmoreを使ってみた

shontauro/android-pulltorefresh-and-loadmoreにあるライブラリは、リストを引っ張ってリストを更新するもので、星が371、Forkが200で、人気のあるライブラリのようです。このライブラリを、AsyncTaskLoaderとListFragmentを組み合わせたサンプルにNavigationViewを導入してみたで紹介したサンプルアプリ andropenguin/CustomListFragmentSample7で使ってみました。CustomListFragmentSample7 では、ライブラリを使用せず、リストを上に引っ張って内容更新を行っていましたが、今回は、shontauro/android-pulltorefresh-and-loadmore のライブラリを使います。また、shontauro/android-pulltorefresh-and-loadmore のサンプルでは、FragmentやAndroid Design Support LibraryのNavigationViewを使っていないので、今回、それらを使うようにしています。

IntelliJで、gradleでビルドするため、Gradle Android Moduleで新規プロジェクトを作り、プロジェクト直下に、shontauro/android-pulltorefresh-and-loadmore の pulltorefresh-and-loadmore ディレクトリをコピーし、settings.gradleに

include ':pulltorefresh-and-loadmore'

の行を追加します。また、app/build.gradleに

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.0'
    compile project(':pulltorefresh-and-loadmore')
    compile 'com.android.support:support-annotations:22.2.0'
    compile 'com.android.support:appcompat-v7:22.2.0'
    compile 'com.android.support:support-v4:22.2.0'
    compile 'com.android.support:design:22.2.0'
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'
}

を記述します。

主要なレイアウトファイルは、次の通りです。

layout/activity_main.xml

<android.support.v4.widget.DrawerLayout
        android:id="@+id/main_drawer_layout"
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

    <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <android.support.v7.widget.Toolbar
                android:id="@+id/main_tool_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="?attr/colorPrimary"
                android:minHeight="?attr/actionBarSize"
                app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>

        <FrameLayout
                android:id="@+id/main_frame"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_below="@id/main_tool_bar"/>

    </RelativeLayout>

    <android.support.design.widget.NavigationView
            android:id="@+id/main_drawer_view"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="start"
            app:headerLayout="@layout/drawer_header"
            app:menu="@menu/main_drawer"/>

</android.support.v4.widget.DrawerLayout>

layout/drawer_header.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="178dp"
                android:background="@color/secondary_text">

</RelativeLayout>

layout/list_item.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text=""/>

</LinearLayout>

layout/loadmore.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical" >

    <!-- We have to indicate that the listview is now a LoadMoreListView -->

    <com.costum.android.widget.LoadMoreListView
            android:id="@+id/android:list"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

</LinearLayout>

loadmore.xmlは、Fragmentのレイアウトファイルになっていて、com.costum.android.widget.LoadMoreListViewタグを内包します。

次に、リスト更新に関係するJavaファイルは次の通りです。

CustomListFragment.java

import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.costum.android.widget.LoadMoreListView;
import java.util.List;

public class CustomListFragment extends ListFragment
        implements LoadMoreListView.OnLoadMoreListener,
        LoaderManager.LoaderCallbacks<List<Entry>> {
    private CustomAdapter mCustomAdapter;
    private boolean mIsLoading = false;
    private int mCount = 0;

    private final static String TAG = CustomListFragment.class.getSimpleName();

    public CustomListFragment() {
    }

    public static CustomListFragment newInstance() {
        CustomListFragment fragment = new CustomListFragment();
        return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if (container == null) {
            return null;
        }
        return inflater.inflate(R.layout.loadmore, container, false);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        mCustomAdapter = new CustomAdapter(getActivity());
        setListAdapter(mCustomAdapter);
        ((LoadMoreListView)getListView()).setOnLoadMoreListener(this);
        mIsLoading = true;
        getLoaderManager().initLoader(0, null, this);
    }

    @Override
    public void onLoadMore() {
        Log.d(TAG, "mIsLoading = " + mIsLoading);
        // 既に読み込み中ならスキップ
        if (mIsLoading) {
            return;
        }
        mIsLoading = true;
        getLoaderManager().initLoader(mCount, null, this);
    }

    @Override
    public Loader<List<Entry>> onCreateLoader(int id, Bundle args) {
        return new CustomLoader(getActivity(), mCount);
    }

    @Override
    public void onLoadFinished(Loader<List<Entry>> loader, List<Entry> data) {
        // Call onLoadMoreComplete when the LoadMore task, has finished
        ((LoadMoreListView) getListView()).onLoadMoreComplete();
        mCustomAdapter.setData(data);
        mIsLoading = false;
        mCount++;
    }

    @Override
    public void onLoaderReset(Loader<List<Entry>> loader) {
        mCustomAdapter.setData(null);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // java.lang.IllegalStateException: Content view not yet created となる。
//        ((LoadMoreListView)getListView()).setOnLoadMoreListener(null);
        setListAdapter(null);
        mCustomAdapter.setData(null);
        mCustomAdapter = null;
    }
}

pull-load-example3/app/src/main/java/com/sarltokyo/pull_load_example3/app/CustomListFragment.javaによると、リストを上に引っ張って内容を更新するには、com.costum.android.widget.LoadMoreListView クラスを使い、LoadMoreListView.OnLoadMoreListener インターフェースを実装すればいいようです。

onActivityCreatedメソッド内で、

((LoadMoreListView)getListView()).setOnLoadMoreListener(this);

によってリスナーを設定した後、AsyncTaskLoaderを初期化します。

onLoadMoreメソッドでは、リストを更新するので、mCountで、AsyncTaskLoaderを初期化し直します。

onLoadFinishedメソッドでは、リストの更新が終了したことを伝えるために、

((LoadMoreListView) getListView()).onLoadMoreComplete();

とします。また、mCountを1、増やします.

onDestroyメソッドでは、次のように、メモリーリーク対策を行っていますが、不十分なようです。

        setListAdapter(null);
        mCustomAdapter.setData(null);
        mCustomAdapter = null;

プロジェクトを、

andropenguin/pull-load-example3

に公開します。

参考サイト
[Android]OutOfMemoryError(メモリリーク)対策
Android Design Support Library を少しだけ触ってみました
shontauro/android-pulltorefresh-and-loadmore