Wednesday, April 22, 2015

MyNotes: Loaders

So I now have data and I have a rudimentary UI. Now I how did I get them to work together?  The answer: Loaders.
Loaders are fun.  They take care of the data synchronizations issue for you.  If the data updates in the database, the loader gets notified.  This makes things really nice in an app that is going to be manipulating the database via user input.
In some cases the notifications are automatically.  I didn’t get that lucky.  I already built my data layer and was happy with it before I decided to use Loaders so I get to write my own.  Meet the FilteredNoteLoader.

FilteredNoteLoader

/**
 * The loader to retrieve the filtered note list and listen for any changes to it.
 */
public class FilteredNoteLoader extends AsyncTaskLoader<List<Note>> {

    /**
     * The filtered notes retrieved from persistent storage.
     */
    private List<Note> mNotes;


    /**
     * The notes manager that retrieves data from persistent storage.
     */
    private final NotesManager mNotesManager;


    /**
     * The filter manager that retrieves data from persistent storage.
     */
    private final NoteFilterCache mNoteFilterCache;


    /**
     * A local broadcast manager to listen for broadcasts upon any changes to the notes or note
     * filter.
     */
    private final LocalBroadcastManager mBroadcastManager;


    /**
     * A flag that indicates that the loader is monitoring changes to the notes or note filter.
     */
    private boolean mObserverRegistered;


    /**
     * The broadcast receiver to handle any changes in the note or note filter.
     */
    private final BroadcastReceiver mOnChangeBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, final Intent intent) {
            onContentChanged();
        }
    };


    /**
     * Constructor.
     *
     * @param context
     *         The context to associate with this loader.
     */
    public FilteredNoteLoader(@NonNull Context context) {
        super(context);

        mNotesManager = new NotesManager(DatabaseHelper.getDatabase());
        mBroadcastManager = LocalBroadcastManager.getInstance(context);
        mNoteFilterCache = new NoteFilterCache(context);
    }


    @Override
    public List<Note> loadInBackground() {
        Set<NoteColor> enabledFilters = mNoteFilterCache.getEnabledColors();
        List<Note> data = mNotesManager.getNotesWithColors(new ArrayList<>(enabledFilters));

        return data;
    }


    @Override
    public void deliverResult(List<Note> data) {
        if (isReset()) {
            return;
        }

        mNotes = data;

        if (isStarted()) {
            super.deliverResult(data);
        }
    }


    @Override
    protected void onStartLoading() {
        if (mNotes != null) {
            deliverResult(mNotes);
        }

        registerReceivers();

        if (takeContentChanged() || mNotes == null) {
            forceLoad();
        }
    }


    @Override
    protected void onStopLoading() {
        cancelLoad();
    }


    @Override
    protected void onReset() {
        onStopLoading();

        if (mNotes != null) {
            mNotes = null;
        }

        unregisterReceivers();
    }


    /**
     * Register the observer to monitor changes to the note or note filter.
     */
    private void registerReceivers() {
        if (!mObserverRegistered) {
            mBroadcastManager.registerReceiver(mOnChangeBroadcastReceiver,
                                               NoteFilterBroadcast.createIntentFilter());

            mBroadcastManager.registerReceiver(mOnChangeBroadcastReceiver,
                                               NoteTableChangeBroadcast.createIntentFilter());

            mObserverRegistered = true;
        }
    }


    /**
     * Stop monitoring changes to the note and note filter.
     */
    private void unregisterReceivers() {
        mObserverRegistered = false;

        mBroadcastManager.unregisterReceiver(mOnChangeBroadcastReceiver);
    }
}

This class reads from two persistent sources:  The database is accessed via NotesManager and the selected color filters via NoteFilterCache (a façade to the PreferenceManager for saving the filter note colors). The loader queries for the filtered notes and then stores them for later use. It will automatically notify any listeners of the updated data. 

The one big glitch is that the Loader requires a data observer to monitor the data sources.  I have to provide that observer. My solution is too use BroadcastReceivers.  When any note commits a change to the database it also sends a broadcast about the change.  The loader listens for the broadcast and marks the content as changed.  It does the same thing for the note filter cache.

Here is the magic code from Note to make this work:

    /**
     * @throws com.fsk.common.threads.ThreadException
     *         when call from the UI thread.
     */
    @Override
    public void save(final SQLiteDatabase db) {

        ThreadCheck.checkOffUIThread();
        long row = db.insertWithOnConflict(Tables.NOTES, null, createContentValues(),
                                           SQLiteDatabase.CONFLICT_REPLACE);

        if (row != NOT_STORED) {
            mId = row;
            MyNotesApplication.sendLocalBroadcast(NoteTableChangeBroadcast.createIntent());
        }
    }


    /**
     * @throws com.fsk.common.threads.ThreadException
     *         when call from the UI thread.
     */
    @Override
    public void delete(final SQLiteDatabase db) {

        ThreadCheck.checkOffUIThread();
        if (getId() != NOT_STORED) {
            int deletedRows = db.delete(Tables.NOTES, Columns.NOTE_ID + " = ?",
                                        new String[] { Long.toString(mId) });
            if (deletedRows > 0) {
                setId(NOT_STORED);
                MyNotesApplication.sendLocalBroadcast(NoteTableChangeBroadcast.createIntent());
            }
        }
    }


For the record, if I had used a ContentProvider for my data access layer I wouldn’t need this setup.  I would just use a CursorLoader instead.

Hooking it Up


I now need to hook up the Loader to my Activity.  The Activity provides a LoaderManager.  Just register the loader into the LoaderManager and then implement the LoaderManager.LoaderCallbacks interface.

public class MainActivity extends Activity implements LoaderManager.LoaderCallbacks<List<Note>> {

    /**
     * The identifier of the main loader.  This loads the color filtered note list.
     */
    static final int MAIN_LOADER_ID = 0;

    ...

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        getLoaderManager().initLoader(MAIN_LOADER_ID, null, this);
    }


    @Override
    public Loader<List<Note>> onCreateLoader(final int id, final Bundle args) {
        return new FilteredNoteLoader(this);
    }


    @Override
    public void onLoadFinished(final Loader<List<Note>> loader, final List<Note> data) {
        mCardAdapter.setNotes(data);
    }


    @Override
    public void onLoaderReset(final Loader<List<Note>> loader) {
        //nothing
    }
}

Commits


The commits associated with these changes can be found at github here.

No comments :

Post a Comment