Custom Loaders with SQLite in Android

By | June 1, 2016

Introduced in Android 3.0, loaders make it easy to asynchronously load data in an activity or fragment. Loaders have these characteristics:

  • They are available to every Activity and Fragment.
  • They provide asynchronous loading of data.
  • They monitor the source of their data and deliver new results when the content changes.
  • They automatically reconnect to the last loader’s cursor when being recreated after a configuration change. Thus, they don’t need to re-query their data.

You can read more about Loaders from here.

Read Loaders with SimpleCursorAdapter from here.

We will create a Sqlite Database with a Table and three columns, id, name and company.

We will try to query data with Loaders and SimpleCursorAdapter.

The Loaders will load the data from the database to the ListView.

Where there is new Data added, the Loader will automatically reload the data in the ListView.

Lets see how to do this.

I am using a class called DemoItem which contains three members – name and company.
Our Sqlite database has the same structure with an autoincrement ID.


package com.coderzheaven.myapplication;

public class DemoItem {

    String name, company;

    public String getName() {
        return name;
    }

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

    public String getCompany() {
        return company;
    }

    public void setCompany(String company) {
        this.company = company;
    }

    public void set(String name, String company) {
        this.name = name;
        this.company = company;
    }
}

Here is our Database Helper class


package com.coderzheaven.myapplication;


import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

import java.util.LinkedList;
import java.util.List;

public class MySQLiteHelper extends SQLiteOpenHelper {

    // Database Version
    private static final int DATABASE_VERSION = 1;
    // Database Name
    private static final String DATABASE_NAME = "DemoDB";

    private static final String TABLE_NAME = "Demo";

    public static final String KEY_ID = "_id";
    public static final String KEY_NAME = "NAME";
    public static final String KEY_COMPANY = "COMPANY";

    private static final String[] COLUMNS = {KEY_ID, KEY_NAME, KEY_COMPANY};

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

    @Override
    public void onCreate(SQLiteDatabase db) {

        String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
                + KEY_ID + " INTEGER primary key AUTOINCREMENT,"
                + KEY_NAME + " TEXT,"
                + KEY_COMPANY + " TEXT);";

        db.execSQL(CREATE_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);

        this.onCreate(db);
    }

    public void addItem(DemoItem item) {

        // Get reference to writable DB
        SQLiteDatabase db = this.getWritableDatabase();

        // Create ContentValues to add key "column"/value
        ContentValues values = new ContentValues();
        values.put(KEY_NAME, item.getName()); // get title
        values.put(KEY_COMPANY, item.getCompany()); // get author

        // Insert
        db.insert(TABLE_NAME, // table
                null, //nullColumnHack
                values); // key/value -> keys = column names/ values = column values

        // close the db
        db.close();

    }

    public List<DemoItem> getAllItems() {
        List<DemoItem> items = new LinkedList<DemoItem>();

        String query = "SELECT  * FROM " + TABLE_NAME;

        SQLiteDatabase db = this.getWritableDatabase();
        Cursor cursor = db.rawQuery(query, null);

        DemoItem item = null;
        if (cursor.moveToFirst()) {
            do {
                item = new DemoItem();
                item.setId(cursor.getString(0));
                item.setName(cursor.getString(1));
                item.setCompany(cursor.getString(2));
                items.add(item);
            } while (cursor.moveToNext());
        }

        return items;
    }

}

Below is my Adapter class for the ListView.

package com.coderzheaven.myapplication;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

import java.util.List;

public class CustomAdapter extends BaseAdapter{

    private Context context;
    private List<demoitem> data;
    private static LayoutInflater inflater = null;

    public CustomAdapter(Context context, List</demoitem><demoitem> data) {

        this.context = context;
        this.data = data;

        inflater = (LayoutInflater) context.
                getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    }

    public int getCount() {
        return data.size();
    }

    public Object getItem(int position) {
        return position;
    }

    public long getItemId(int position) {
        return position;
    }

    public static class ViewHolder {

        public TextView id;
        public TextView name;
        public TextView company;

    }

    public View getView(int position, View convertView, ViewGroup parent) {

        View vi = convertView;
        ViewHolder holder;

        if (convertView == null) {

            vi = inflater.inflate(R.layout.list_row, null);
            holder = new ViewHolder();
            holder.id = (TextView) vi.findViewById(R.id.text0);
            holder.name = (TextView) vi.findViewById(R.id.text1);
            holder.company = (TextView) vi.findViewById(R.id.text2);

            vi.setTag(holder);

        } else {

            holder = (ViewHolder) vi.getTag();

        }

        DemoItem demoItem = data.get(position);
        holder.id.setText("ID = " + demoItem.getId());
        holder.name.setText(demoItem.getName());
        holder.company.setText(demoItem.getCompany());

        return vi;
    }
}


<font color="orange">The Custom Loader Class.</font>

[java]
package com.coderzheaven.myapplication;

import android.content.AsyncTaskLoader;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.util.Log;

import java.util.List;

public class SampleLoader extends AsyncTaskLoader<List<DemoItem>> {

    public static final String TAG = "SampleLoader";

    // We hold a reference to the Loader’s data here.
    private List<DemoItem> mData;

    Context ctx;

    public SampleLoader(Context ctx) {
        super(ctx);
        this.ctx = ctx;
        ctx.registerReceiver(mObserver, new IntentFilter("my-event"));
    }

    @Override
    public List<DemoItem> loadInBackground() {
        // This method is called on a background thread and should generate a
        // new set of data to be delivered back to the client.

        MySQLiteHelper mySQLiteHelper = new MySQLiteHelper(ctx);
        List<DemoItem> allItems = mySQLiteHelper.getAllItems();

        Log.i(TAG,"Size : " + allItems.size());

        return allItems;
    }

    @Override
    public void deliverResult(List<DemoItem> data) {
        if (isReset()) {
            // The Loader has been reset; ignore the result and invalidate the data.
            releaseResources(data);
            return;
        }

        // Hold a reference to the old data so it doesn't get garbage collected.
        // We must protect it until the new data has been delivered.
        List<DemoItem> oldData = mData;
        mData = data;

        if (isStarted()) {
            // If the Loader is in a started state, deliver the results to the
            // client. The superclass method does this for us.
            super.deliverResult(data);
        }

        // Invalidate the old data as we don't need it any more.
        if (oldData != null && oldData != data) {
            releaseResources(oldData);
        }
    }

    @Override
    protected void onStartLoading() {
        if (mData != null) {
            // Deliver any previously loaded data immediately.
            deliverResult(mData);
        }

        if (takeContentChanged() || mData == null) {
            // When the observer detects a change, it should call onContentChanged()
            // on the Loader, which will cause the next call to takeContentChanged()
            // to return true. If this is ever the case (or if the current data is
            // null), we force a new load.
            forceLoad();
            Intent intent = new Intent("my-event");
            intent.putExtra("message", "message");
            getContext().sendBroadcast(intent);
        }
    }

    @Override
    protected void onStopLoading() {
        // The Loader is in a stopped state, so we should attempt to cancel the
        // current load (if there is one).
        cancelLoad();

        // Note that we leave the observer as is. Loaders in a stopped state
        // should still monitor the data source for changes so that the Loader
        // will know to force a new load if it is ever started again.
    }

    @Override
    protected void onReset() {
        // Ensure the loader has been stopped.
        onStopLoading();

        // At this point we can release the resources associated with 'mData'.
        if (mData != null) {
            releaseResources(mData);
            mData = null;
        }

        // The Loader is being reset, so we should stop monitoring for changes.
        if (mObserver != null) {
            try {
                getContext().unregisterReceiver(mObserver);
            } catch (Exception e) {
                e.printStackTrace();
            }
            mObserver = null;
        }
    }

    @Override
    public void onCanceled(List<DemoItem> data) {
        // Attempt to cancel the current asynchronous load.
        super.onCanceled(data);

        // The load has been canceled, so we should release the resources
        // associated with 'data'.
        releaseResources(data);
    }

    private void releaseResources(List<DemoItem> data) {
        // For a simple List, there is nothing to do. For something like a Cursor, we
        // would close it in this method. All resources associated with the Loader
        // should be released here.
    }

    private BroadcastReceiver mObserver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // Extract data included in the Intent
            //String message = intent.getStringExtra("message");
            Log.d("receiver", "Got message: ");
        }
    };
}

Here is the Complete Fragment Class that implements the Custom Loader.


package com.coderzheaven.myapplication;

import android.app.Fragment;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.Context;
import android.content.Loader;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;

import java.util.List;

public class MainActivityFragment extends Fragment implements LoaderCallbacks<List<DemoItem>>, View.OnClickListener {

    private static final String TAG = "MainActivityFragment";
    private static final int LOADER_ID = 1;
    Context context;
    MySQLiteHelper m;
    SampleLoader sampleLoader;
    CustomAdapter customAdapter;
    ListView mList;
    List<DemoItem> allList;
    FloatingActionButton fab;

    public MainActivityFragment() {
        this.context = getActivity();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.fragment_main, container, false);

        fab = (FloatingActionButton) view.findViewById(R.id.fab);
        fab.setOnClickListener(this);

        m = new MySQLiteHelper(getActivity());

        getLoaderManager().initLoader(LOADER_ID, null, this);

        mList = (ListView) view.findViewById(R.id.lv);

        allList = m.getAllItems();
        customAdapter = new CustomAdapter(getActivity(), allList);
        mList.setAdapter(customAdapter);

        return view;
    }

    public void add() {

        DemoItem d = new DemoItem();
        d.set("CoderzHeaven", "Company");
        m.addItem(d);

        sampleLoader.onContentChanged();
    }

    @Override
    public Loader<List<DemoItem>> onCreateLoader(int id, Bundle args) {
        sampleLoader = new SampleLoader(getActivity());
        return sampleLoader;
    }

    @Override
    public void onLoadFinished(
            Loader<List<DemoItem>> loader,
            List<DemoItem> cursor) {
        Log.i(TAG, "onLoadFinished " + cursor.size());
        allList.clear();
        allList.addAll(m.getAllItems());
        customAdapter.notifyDataSetChanged();
        mList.setSelection(allList.size() - 1);
    }

    @Override
    public void onLoaderReset(Loader<List<DemoItem>> loader) {
        Log.i(TAG, "onLoaderReset");
    }

    @Override
    public void onClick(View v) {
        if (v == fab)
            add();
    }
}

You can download the complete source code from here.

3 thoughts on “Custom Loaders with SQLite in Android

  1. caviru

    amazing tutorial this is what i am looking for……please the source code this turoial.the above link is broken

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *