Showing posts with label Loader. Show all posts
Showing posts with label Loader. Show all posts

Wednesday, June 17, 2015

MyNotes: We all Make Mistakes

 

In one of my last posts I moved all of the logic into the MainActivity.  I changed my mind about that.  I liked the having the fragments more so I reintroduced two fragments: NoteCardsFragment and the ColorFilterFragment.

I then moved the most of the logic out of the MainActivity and into those fragments.

NoteCardsFragment

The same says it all.  This is a fragment that displays all the notes discovered by the loader.  This fragment really just consists of a Loader (moved from the MainActivity), a RecyclerView, and the supporting code to create the RecyclerView. 


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


/**
* UI element to display the note data.
*/
@InjectView(R.id.fragment_note_cards_recycler_view)
RecyclerView mCardsRecyclerView;


/**
* The adapter to manage the display of cards in {@link #mCardsRecyclerView}.
*/
CardAdapter mCardAdapter;


/**
* The listener to a note being clicked.
*/
final CardAdapter.OnItemClickListener mNoteClickListener =
new CardAdapter.OnItemClickListener() {

@Override
public void onItemClick(final View view, final Note note) {
editNote(view, note);
}
};


@Nullable
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_note_cards, container, false);
ButterKnife.inject(this, rootView);

Context context = inflater.getContext();
mCardAdapter = new CardAdapter();
mCardAdapter.setOnItemClickListener(mNoteClickListener);

Resources resources = context.getResources();
int columnCount = resources.getInteger(R.integer.card_column_count);
float cardDividerDimen = resources.getDimension(R.dimen.note_divider_size);

mCardsRecyclerView.addItemDecoration(new DividerItemDecoration((int) cardDividerDimen));
mCardsRecyclerView.setLayoutManager(new GridLayoutManager(context, columnCount));
mCardsRecyclerView.setAdapter(mCardAdapter);

return rootView;
}


@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(MAIN_LOADER_ID, null, this);
}


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


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


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


At this point, the biggest lesson to take away is that I don’t create the loader until onActivityCreated.  This is very important because it is the earliest point in the lifecycle that I can guarantee that both the Fragments main view (getView()) and the Activity are ready.




@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(MAIN_LOADER_ID, null, this);
}


One other minor thing to note is that I normally do a public static method that creates my Fragments.  These are normally named something like newInstance.



/**
* Create a new instance of the {@link NoteCardsFragment}
*
* @return a new instance of the {@link NoteCardsFragment}
*/
public static NoteCardsFragment newInstance() {
return new NoteCardsFragment();
}



Normally, Fragment creation goes something like this:


Fragment fragment = new MyFragment();
Bundle arguments = new Bundle();
arguments.putInt(KEY, VALUE);
fragment.setArguments(arguments);

It may not seem like it matters but the newInstance code (along with make all the argument keys private) converts a runtime bugs into a compile time bugs.  To me that is a huge deal, especially on larger projects.


ColorFilterFragment


This fragment is responsible for filtering notes by color.  It is just the RecyclerView and its supporting data.  This was directly ported from the MainActivity.


public class ColorFilterFragment extends Fragment {

/**
* Create a new instance of the {@link ColorFilterFragment}
*
* @return a new instance of the {@link ColorFilterFragment}
*/
public static ColorFilterFragment newInstance() {
return new ColorFilterFragment();
}


/**
* UI element that allows the user to filter notes by color.
*/
@InjectView(R.id.fragment_color_filter_recycler)
RecyclerView mFilterRecyclerView;


/**
* The Cache for the selected colors.
*/
NoteFilterCache mNoteFilterCache;


/**
* The listener to react to changes to the color filter selection.
*/
final FilterColorFilterAdapter.OnColorChangeListener mOnColorSelectChangeListener =
new FilterColorFilterAdapter.OnColorChangeListener() {
@Override
public void onColorSelected(final NoteColor color, final boolean enabled) {
mNoteFilterCache.enableColor(color, enabled);
}
};


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

View rootView = inflater.inflate(R.layout.fragment_color_selector, container, false);
ButterKnife.inject(this, rootView);

Context context = inflater.getContext();
mNoteFilterCache = new NoteFilterCache(context);

Resources resources = context.getResources();
mFilterRecyclerView.setAdapter(createAdapter(context));
mFilterRecyclerView.addItemDecoration(createDividerItemDecoration(resources));
mFilterRecyclerView.setLayoutManager(createLayoutManager(context, resources));

return rootView;
}


/**
* Create the adapter.
*
* @param context
* The context to use for creating the adapter.
*
* @return The adapter for managing {@link #mFilterRecyclerView}.
*/
private FilterColorFilterAdapter createAdapter(@NonNull Context context) {
FilterColorFilterAdapter adapter = new FilterColorFilterAdapter(context);
adapter.setOnColorChangeListener(mOnColorSelectChangeListener);
adapter.setSelectedColors(mNoteFilterCache.getEnabledColors());

return adapter;
}


/**
* Create a {@link DividerItemDecoration} for the RecyclerView.
*
* @param resources
* The resources to read for divider and orientation data.
*
* @return a new {@link DividerItemDecoration}.
*/
private DividerItemDecoration createDividerItemDecoration(Resources resources) {
int dividerHorizontal =
(int) resources.getDimension(R.dimen.color_filter_item_divider_horizontal);
int dividerVertical =
(int) resources.getDimension(R.dimen.color_filter_item_divider_vertical);

return new DividerItemDecoration(dividerVertical, dividerHorizontal);
}


/**
* Create a {@link ColorFilterLayoutManager} for the RecyclerView.
*
* @param context
* The context to assign to the layout manager.
* @param resources
* The resources to read for divider and orientation data.
*
* @return a new {@link ColorFilterLayoutManager}.
*/
private RecyclerView.LayoutManager createLayoutManager(Context context, Resources resources) {
boolean vertical = resources.getBoolean(R.bool.color_filter_toolbar_vertical);
int orientation = vertical ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL;
return new ColorFilterLayoutManager(context, orientation);
}
}

MainActivity


Last, but not least, is the MainActivity.  This class was gutted of most of the business logic.  It is now responsible for adding notes and adding the above fragments. 


/**
* The primary activity. It provides a list of notes and the tools to manage them.
*/
public class MainActivity extends AppCompatActivity {

/**
* The fragment tags.
*/
static class FragmentTags {
/**
* The tag for the note cards fragment.
*/
public static final String NOTE_CARDS_FRAGMENT_TAG = "NOTE_CARDS_FRAGMENT_TAG";

/**
* The tag for the color filter fragment.
*/
public static final String COLOR_FILTER_FRAGMENT_TAG = "COLOR_FILTER_FRAGMENT_TAG";
}

/**
* UI element that displays a toolbar
*/
@InjectView(R.id.activity_main_toolbar)
Toolbar mToolbar;


/**
* UI element that allows the user to add a note.
* @param view
*/
@OnClick(R.id.activity_main_add_view)
public void addViewClick(View view) {
createNewNote();
}


@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.inject(this);

// Set a toolbar to replace the action bar.
Toolbar toolbar = (Toolbar) findViewById(R.id.activity_main_toolbar);
setSupportActionBar(toolbar);

Fragment colorFilterSelectorFragment = ColorFilterFragment.newInstance();
Fragment noteCardsFragment = NoteCardsFragment.newInstance();

FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(R.id.activity_main_notes_container,
noteCardsFragment,
FragmentTags.NOTE_CARDS_FRAGMENT_TAG);

transaction.replace(R.id.activity_main_color_filter_container,
colorFilterSelectorFragment,
FragmentTags.COLOR_FILTER_FRAGMENT_TAG);
transaction.commit();
}


/**
* Launch a UI that allows the user to create a new note.
*/
private void createNewNote() {
startActivity(EditNoteActivity.createIntent(this, null));
}
}



Commits


These changes can be found at commits https://github.com/fsk-software/mynotes/commit/a0632503c5fabf74a31cca091d7975419d002328 and https://github.com/fsk-software/mynotes/commit/ec427c6a5684220836f823824d58a61cd2aea47b.

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.