Using Loaders in Android Using SimpleCursorAdapter in Android.

By | May 7, 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.

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;

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) {

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

        // Execute the Query
        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);
    }

    /*
      Add Records to the Database
     */
    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

        // Do Insert
        db.insert(TABLE_NAME, null, values);

        // Close the DB
        db.close();

    }

    /*
        Get all records from the Database
     */
    public Cursor getAllItemsCursor() {

        String query = "SELECT  * FROM " + TABLE_NAME;

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

        return cursor;
    }
}

Now we will write our Custom Loader for Loading values from the SqliteDatabase.

Create a new Class called “MyLoader” and copy this contents to it.

package com.coderzheaven.myapplication;

import android.content.AsyncTaskLoader;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;

public class MyLoader extends AsyncTaskLoader<Cursor> {

    public static final String TAG = "MyLoader";

    // Database Helper Class
    MySQLiteHelper mySQLiteHelper;

    Cursor cursor;

    // We hold a reference to the Loader’s data here.
    private Cursor mData;

    Context ctx;

    public MyLoader(Context ctx) {
        super(ctx);
        this.ctx = ctx;
    }

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

        // Initialize Database Helper Class
        mySQLiteHelper = new MySQLiteHelper(ctx);

        // Get all records from the Database
        cursor = mySQLiteHelper.getAllItemsCursor();

        return cursor;
    }

    @Override
    public void deliverResult(Cursor 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.
        Cursor 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(Cursor 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(Cursor 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.
    }

}

Now we have My Custom SimpleCursorAdapter for loading data in ListView.

package com.coderzheaven.myapplication;

import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.view.ViewGroup;
import android.widget.SimpleCursorAdapter;

public class MyCursorAdapter extends SimpleCursorAdapter {

    public MyCursorAdapter(Context context, int layout, Cursor c,
                           String[] from, int[] to, int flags) {
        super(context, layout, c, from, to, flags);
    }

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

        //get reference to the row
        View view = super.getView(position, convertView, parent);

        return view;
    }

}

Now will go into the implementation.

To initialize a Loader, you could simply call..

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

You need to implement LoaderManager.LoaderCallbacks to work with Loaders.

The methods you have to implement are:

  • onCreateLoader() – The LoaderManager calls this method when you call initLoader() for the first time. As mentioned, the manager only calls this method if no loader for the given ID exists.
  • onLoadFinished() – Here you update the UI based on the results of your query.
  • onLoadReset() – Release any resources you hold, so that the Loader can free them.

Below is the Fragment that implements the Above Loader.


package com.coderzheaven.myapplication;

import android.app.Fragment;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.Context;
import android.content.Loader;
import android.database.Cursor;
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;

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

    private static final String TAG = "MainActivityFragment";
    private static final int LOADER_ID = 1;
    Context context;
    MySQLiteHelper m;
    MyLoader myLoader;
    ListView mList;
    MyCursorAdapter simpleCursorAdapter;
    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);

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

        // Initialize the Database Helper Class
        m = new MySQLiteHelper(getActivity());

        // Initialize our Loader
        getLoaderManager().initLoader(LOADER_ID, null, this);

        // Get all the records from the database in a Cursor
        Cursor cursor = m.getAllItemsCursor();

        // The desired columns to be bound
        String[] columns = new String[]{
                MySQLiteHelper.KEY_ID,
                MySQLiteHelper.KEY_NAME,
                MySQLiteHelper.KEY_COMPANY
        };

        // The XML defined views which the data will be bound to
        int[] to = new int[]{
                R.id.text0,
                R.id.text1,
                R.id.text2
        };

        // Initialize our Cursor Adapter
        simpleCursorAdapter = new MyCursorAdapter(
                getActivity(),
                R.layout.list_row,
                cursor,
                columns,
                to,
                0);

        // Set the adapter to our ListView
        mList.setAdapter(simpleCursorAdapter);

        // Return the view for the fragment.
        return view;
    }

    /**
     * Add the data to the database
     */
    public void add() {

        // Create new Item and add Values
        DemoItem d = new DemoItem();
        d.set("CoderzHeaven", "Company");

        // Insert the item to the Database
        m.addItem(d);

        // Call whenever content is changed for the Loader
        myLoader.onContentChanged();
    }

    /*
    This function will be called when you initalize a Loader
     */
    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {

        // Initialize our Custom Loader
        myLoader = new MyLoader(getActivity());
        return myLoader;
    }


    /*
      When the cursor is finally available you have to add it. You do this by calling         
      swapCursor() on your adapter and passing in the cursor object of your callback method
    */
    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        Log.i(TAG, "onLoadFinished " + cursor.getCount());
        // Notify the adapter to change the values
        simpleCursorAdapter.swapCursor(cursor);
        //Set the Selection to the last row in the ListView.
        mList.setSelection(cursor.getCount() - 1);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        Log.i(TAG, "onLoaderReset");
        simpleCursorAdapter.swapCursor(null);
    }

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

When not to use Loaders

  • You shouldn’t use Loaders if you need the background tasks to complete.
  • Android destroys Loaders together with the Activities/Fragments they belong to.
  • If you want to do some tasks, that have to run until completion even after your application/Activity is destroyed, do not use Loaders. You should use services for this kind of stuff instead.

You can download the complete Android Studio Source code from here.

CUSTOM LOADERS COMING SOON IN THE NEXT ARTICLE, SUBSCRIBE TO OUR NEWSLETTER.

Leave a Reply

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