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.

Wednesday, April 15, 2015

My Notes: UI structure

I am planning on doing a hybrid fragment-oriented design.  The activity will display the notes, but the note editor, toolbars, etc. will be fragments. 
I considered having the notes display in its own fragment, but that seems very messy with the interaction with the add button and other fragments.  Besides, the activity was looking a little sparse in that design.
Right now I only have the MainActivity and the MainToolbarFragment.  Since a picture is worth a thousand words, here it is:
image

MainActivity

The MainActivity is the entry point for the app.  It is the primary (and maybe only) screen that the user will see.  It will directly manage the presentation and creation of the note data.  The notes are displayed in a RecyclerView that displays individual notes in CardViews.  The relevant code in activity is:

/**
 * The primary activity.  It provides a list of notes and the tools to manage them.
 */
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;


    /**
     * UI element that manages the primary toolbar.
     */
    MainToolbarFragment mMainToolbarFragment;


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


    /**
     * UI element to allow a user to create a new note.
     */
    @InjectView(R.id.activity_main_add_view)
    View mAddView;


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


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

        mCardAdapter = new CardAdapter(null);

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

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

        FragmentManager manager = getFragmentManager();
        FragmentTransaction transaction = manager.beginTransaction();

        mMainToolbarFragment = new MainToolbarFragment();

        transaction.replace(R.id.activity_main_toolbar, mMainToolbarFragment);

        transaction.commit();
        getLoaderManager().initLoader(MAIN_LOADER_ID, null, this);
    }
...
}

As stated above, this class uses a RecyclerView instead of a ListView.  This component is new to lollipop and improves upon the AdapterViews from previous android SDKs.  There are some quirks with them (I’ll cover what I have found so far in another post), but I highly recommend them for new work.  They are more flexible and more efficient than the old AdapterViews.

The xml to support the code can be found in activity_main.xml:





<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/activity_margin">

    <FrameLayout
        android:id="@+id/activity_main_toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/activity_main_notes_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="10dp"
        android:layout_below="@id/activity_main_toolbar" />

    <include
        layout="@layout/include_floating_add_button"
        android:id="@+id/activity_main_add_view"
        android:layout_width="@dimen/add_button_size"
        android:layout_height="@dimen/add_button_size"
        android:layout_marginRight="10dp"
        android:layout_marginEnd="10dp"
        android:layout_marginBottom="10dp"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"/>

</RelativeLayout>

There really isn’t much to say here.  It is pretty straightforward xml.  The only things to note are the include for the add button and styling. 

The include is an xml feature that helps to simplify and reuse the xml.  In this case I did it to keep the xml streamlined.  I tend to find deep indenting hard to read even with the code collapse features in AndroidStudio. 

You will also note that I didn’t use any styles.  I tend to only use them for repeated features (a common text styling, etc) or when I want a common layout but different styles based on the device configurations.  I am not a big fan of using a style for each component.  I think this makes the code hard to read since it reduces the “glancibility” factor for the file.

MainToolbarFragment


This is the only fragment I have defined right now.  It is the primary toolbar for the activity.  It consists of two sections.  The first section is an header bar with a filter button.

image

The second section is the color filter options:

image

The xml to support this can be found in fragment_toolbar.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="5dp"
    android:minHeight="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    android:elevation="@dimen/toolbar_elevation">

    <TextView
        android:text="@string/app_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textColor="@android:color/white"/>

    <com.fsk.common.presentation.components.CheckableImageView
        android:id="@+id/fragment_toolbar_filter_toggle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:layout_marginRight="10dp"
        android:button="@null"
        android:src="@drawable/filter_background"/>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/fragment_toolbar_filter_view"
        android:layout_height="@dimen/note_filter_toolbar_height"
        android:layout_width="match_parent"
        android:layout_below="@id/fragment_toolbar_filter_toggle"
        android:layout_gravity="center"
        android:visibility="gone"/>
</RelativeLayout>

Once again I am using a RecyclerView.  In this case it is for color filter selector. Each item in the RecycleView is a toggle that allows the user turn on or off that color.  By default, I do not show this View.  It only appears when the user toggles the main the filter option.

The code to support the toolbar is found in MainToolbarFragment.java.  This code is a little more complicated so I will break down by behavior.  This is the main setup code:


    /**
     * UI element to show/hide the color filter sub-menu.
     */
    @InjectView(R.id.fragment_toolbar_filter_toggle)
    CheckableImageView mShowFilterToggle;


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

...


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

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

        mShowFilterToggle.setOnCheckedChangeListener(mFilterToggleCheckedChangeListener);

        Context context = inflater.getContext();
        Resources resources = context.getResources();
        int columnCount = resources.getInteger(R.integer.color_filter_column_count);
        float dividerDimension = resources.getDimension(R.dimen.color_filter_divider);

        mFilterRecyclerView.setLayoutManager(new GridLayoutManager(context, columnCount));
        mFilterRecyclerView.addItemDecoration(new DividerItemDecoration((int) dividerDimension));
        mFilterRecyclerView.setAdapter(new FilterColorAdapter(context));

        return view;
    }

This code initializes the UI components for the toolbar.  It is basically, the standard boilerplate for initializing views.

The next part is more interesting.  When the user clicks on the filter I want it to reveal the Color Filter RecyclerView with a nice animation.  My preferred animation is the CircularReveal. 

Unfortunately, that animator is not available in the compatibility library so I can’t use it for non-Lollipop devices.  I still want similar behavior were the filter view appears from the the upper left corner and the collapses back to the same corner.  To do that I created two animators to support that animation, filter_toolbar_open.xml and filter_toolbar_close.xml.

Here is the content of filter_toolbar_close.xml:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:duration="@android:integer/config_longAnimTime">

    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:valueType="floatType"
        android:propertyName="scaleY"/>
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:valueType="floatType"
        android:propertyName="scaleX"/>
</set>

And here is the filter_toolbar_open.xml:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:duration="@android:integer/config_longAnimTime">

    <objectAnimator
        android:valueFrom="0.0"
        android:valueTo="1.0"
        android:valueType="floatType"
        android:propertyName="scaleY"/>
    <objectAnimator
        android:valueFrom="0.0"
        android:valueTo="1.0"
        android:valueType="floatType"
        android:propertyName="scaleX"/>
</set>

Both of these are ObjectAnimators that change the scale from 1 to 0 and 0 to 1 respectively for the close and open.  By default, animations occur around the upper left corner, but I need them to orient to the upper right instead.  This is done by changing the Views pivot position via setPivotX() and setPivotY().

Intermission: Lollipop Logic Breaks


Lollipop changed a lot of stuff.  Some of the new features are supported in the compatibility libraries, but a lot of the really interesting stuff isn’t.  I really want to avoid cluttering the code with tons of logic breaks and multiple methods for lollipop and pre-lollipop. 

For instance, this is what it looks like with the logic breaks in the class:


    /**
     * Show or hide the note filter options.
     *
     * @param enabled
     *         true to show the note filter.  false will hide it.
     */
    private void enableFilterToolbar(boolean enabled) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            startLollipopAnimator(enabled);
        }
        else {
            startOldAnimator(enabled);
        }
    }
    
    private void startLollipopAnimator(boolean start) {
        int x = (mShowFilterToggle.getLeft() + mShowFilterToggle.getRight()) / 2;
        int y = (mShowFilterToggle.getTop() + mShowFilterToggle.getBottom()) / 2;
        
        int radius = getView().getWidth();
        int startRadius = start ? 0 : radius;
        int endRadius = start ? radius : 0;
        
        ViewAnimationUtils.createCircularReveal(mFilterRecyclerView, x, y, startRadius,
                                                        endRadius).start();
    }

    private void startOldAnimator(boolean start) {
        int x = (mShowFilterToggle.getLeft() + mShowFilterToggle.getRight()) / 2;
        int y = (mShowFilterToggle.getTop() + mShowFilterToggle.getBottom()) / 2;
        int animatorResource =
                (start) ? R.animator.filter_toolbar_open : R.animator.filter_toolbar_close;

        int pivotX = (int) getView().getX() + getView().getWidth();
        mFilterRecyclerView.setPivotX(pivotX);
        mFilterRecyclerView.setPivotY(0);

        AnimatorInflater.loadAnimator(getView().getContext(), animatorResource).start();
    }


It is ugly and will be hard to maintain in the long term.  Since these are going to occur all over the place I am opting for different pattern.

Namely, I am going to use a Factory and Builder pattern. Basically, whenever I hit a big logic break like above I a builder class and then extend it into a compatibility version. I then update a factory to return the current version of builder based on the SDK version,

The main builder is used for lollipop and the compat vesion works for non-Lollipop builds.  Instead of having the logic breaks spread across the class, I instead create a different builder object.  Then the logic for the SDK version exists within an encapsulated object.  This simplifies the code and makes it more maintainable.  When it is time to make Lollipop the minimum version I can delete the compat version and then just fix a few simple compile errors.

You can see this pattern on the toolbar reveal animations:

ToolbarAnimatorBuilder.java:

/**
 * A builder to create the toolbar opening and closing animations.
 */
public class ToolbarAnimatorBuilder {

    /**
     * The focal x-coordinate for the animation.
     */
    protected int mX;


    /**
     * The focal y-coordinate for the animation.
     */
    protected int mY;


    /**
     * The widest radius for the animation.
     */
    protected int mRadius;


    /**
     * The target view for the animation.
     */
    protected View mTarget;


    /**
     * A flag to indicate if the target view is opening or closing.  true opens the toolbar.
     */
    protected boolean mReveal;


    /**
     * Set the focal point for the animation.
     *
     * @param x
     *         the coordinate along the x-axis.
     * @param y
     *         the coordinate along the y-axis.
     *
     * @return {@link ToolbarAnimatorBuilder} to allow for call chaining.
     */
    public ToolbarAnimatorBuilder setFocus(int x, int y) {
        mX = x;
        mY = y;

        return this;
    }


    /**
     * Set the widest radius for the animation.
     *
     * @param radius
     *         the widest radius for the animation.
     *
     * @return {@link ToolbarAnimatorBuilder} to allow for call chaining.
     */
    public ToolbarAnimatorBuilder setRadius(int radius) {
        mRadius = radius;

        return this;
    }


    /**
     * Set the target for the animation.
     *
     * @param target
     *         the {@link View} being revealed or hidden.
     *
     * @return {@link ToolbarAnimatorBuilder} to allow for call chaining.
     */
    public ToolbarAnimatorBuilder setTarget(View target) {
        mTarget = target;
        return this;
    }


    /**
     * Set a status indicating if the toolbar is being revealed or hidden.
     *
     * @param reveal
     *         true to reveal the toolbar.
     *
     * @return {@link ToolbarAnimatorBuilder} to allow for call chaining.
     */
    public ToolbarAnimatorBuilder setReveal(boolean reveal) {
        mReveal = reveal;
        return this;
    }


    /**
     * Build the {@link android.animation.Animator} based on the supplied configuration.
     *
     * @return the created animator.
     *
     * @throws java.lang.NullPointerException
     *         when the target view is null.
     * @throws java.lang.IllegalArgumentException
     *         when the radius is negative.
     */
    public Animator build() {
        validate();
        Animator returnValue = createAnimator();
        returnValue.setTarget(mTarget);
        returnValue.addListener(new ToolbarAnimatorListener(getEndVisibility(), mTarget));

        return returnValue;
    }


    /**
     * Validate the configuration is okay for building an {@link Animator}.
     *
     * @throws java.lang.NullPointerException
     *         when the target view is null.
     * @throws java.lang.IllegalArgumentException
     *         when the radius is negative.
     */
    protected void validate() {
        Preconditions.checkNotNull(mTarget);
        Preconditions.checkArgument(mRadius >= 0);
    }


    /**
     * Create the animator for the supplied configuration.
     *
     * @return an {@link android.animation.Animator} to hide or show the target.
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    protected Animator createAnimator() {
        return ViewAnimationUtils
                .createCircularReveal(mTarget, mX, mY, getStartRadius(), getEndRadius());
    }


    /**
     * Get the visibility for the target view after the animation completes.
     *
     * @return {@link android.view.View#INVISIBLE}, {@link android.view.View#VISIBLE}, or {@link
     * android.view.View#GONE}
,.     */
    protected int getEndVisibility() {
        return (mReveal) ? View.VISIBLE : View.GONE;
    }


    /**
     * Get the starting radius for animation.
     *
     * @return the starting radius for the animation.
     */
    protected int getStartRadius() {
        return (mReveal) ? 0 : mRadius;
    }


    /**
     * Get the final radius for animation.
     *
     * @return the final radius for the animation.
     */
    protected int getEndRadius() {
        return (mReveal) ? mRadius : 0;
    }
}

This provides methods to setup the target of the animations, the focal point, the radius (needed for the circular reveal) and the type of reveal (hide or show).  Once the properties for the animator are set, it can be built via the createAnimator() method.  In this case, I am setting up the circular reveal, but in the compat version I use the animator xml files.

ToolbarAnimatorBuilderCompat.java:

/**
 * A builder to create the toolbar opening and closing animations for Pre-Lollipop devices.
 */
public class ToolbarAnimatorBuilderCompat extends ToolbarAnimatorBuilder {
    @Override
    protected Animator createAnimator() {
        int animatorResource =
                (mReveal) ? R.animator.filter_toolbar_open : R.animator.filter_toolbar_close;

        int pivotX = (int) mTarget.getX() + mTarget.getWidth();
        mTarget.setPivotX(pivotX);
        mTarget.setPivotY(0);

        return AnimatorInflater.loadAnimator(mTarget.getContext(), animatorResource);
    }
}

This inherits from the ToolbarAnimatorBuilder and overrides the createAnimator() and changes it to use the xml animators files instead.  I also set the PivotX and PivotY here.  The pivot positions change the focus point for the animation to occur around this point.

Then I added the Factory, AnimationBuilderFactory:




/**
 * A factory to return Animation Builders.
 */
public enum AnimationBuilderFactory {
    ;


    /**
     * Creates the correct version of the {@link ToolbarAnimatorBuilder} to support the current
     * SDK.
     *
     * @return the correct version of the {@link ToolbarAnimatorBuilder} to support the current
     * SDK.
     */
    public static ToolbarAnimatorBuilder getFilterToolbarAnimatorBuilder() {
        return(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) ?
                                         new ToolbarAnimatorBuilder() :
                                         new ToolbarAnimatorBuilderCompat();
    }
}


Back to the Fragment…


In order to use the Builders all I have to do is add a very small logic break into the fragment:

/**
 * Show or hide the note filter options.
 *
 * @param enabled
 *         true to show the note filter.  false will hide it.
 */
private void enableFilterToolbar(boolean enabled) {
    ToolbarAnimatorBuilder builder = AnimationBuilderFactory.getFilterToolbarAnimatorBuilder();

    try {
        int x = (mShowFilterToggle.getLeft() + mShowFilterToggle.getRight()) / 2;
        int y = (mShowFilterToggle.getTop() + mShowFilterToggle.getBottom()) / 2;
        builder.setTarget(mFilterRecyclerView)
               .setFocus(x, y)
               .setRadius(getView().getWidth())
               .setReveal(enabled).build()
               .start();
    }
    catch (NullPointerException npe) {
        //Don't do anything except prevent the crash propagation.
    }
}

Commits


The commits associated with these changes can be found at github here and the factory update can be found at github here.

Wednesday, April 8, 2015

Tips: Fragment

If you have looked at my source code lately, you will notice that I brought in a Fragment.  Fragments were introduced in honeycomb to help deal with the Fragmentation problem in Android.  It is a very flexible object that allows content and logic to move around an Activity easily.  They are not mini-activities or components (views), nor are they anything in-between.  They are fall into unique category that comes complete with its own lifecycle, but that is heavily reliant (and basically slaved) to an owning activity.

There is some controversy around fragments.  I haven’t met anyone that really loves them, but there are plenty of people hate them.  I initially disliked them. A lot.  After using them for a few years, I have come to appreciate them and what they can provide.  That being said,  I think they are complex and have a steep learning curve to do anything really interesting in them (correctly, anyway).  The good news is that once you master them they can be a very powerful tool.

One of the biggest misconception about them is that they are a UI element like activities and views/components.  They are not.  They can create and manage a UI, but it is not required.  A fragment can exist solely to manage data or to be an intermediary between other fragments.

Flavors

Fragments come in the standard variety and DialogFragments.  DialogFragments are Fragments and provide similar behaviors to a Dialog. If it has a UI then it will also look just like a normal Dialog.  It is now a recommended best practice  to make any custom dialog a DialogFragment.  That is because they solve a lot of the Dialog problems (like the memory leak if you forgot to dismiss one).  They are also pretty much the only convenient way to show a dialog from a fragment. 

DialogFragments can also be shown as a standard fragment.  The presentation mode  is not up to the Fragment.  Whether it is shown as a standard fragment or a dialog is driven by how it is added the FragmentManager.  A standard transaction will show it as a normal fragment.  The show() method will display it as a dialog.  Either way, it is better to code the DialogFragment to handle either case.

Activity Interaction

The fragment’s UI (if it has one) is added into a ViewGroup that is present in the activity (either directly or indirectly).  The fragment itself is never added to the UI hierarchy tree.  The Fragment’s onCreateView() returns a View.  It is that view that is added to the View hierarchy tree.

If the View isn’t added to the hierarchy tree, then the UI cannot show.  This can be done by  adding the fragment with only a tag, but not providing a container to display it.   In this case, the fragment has a UI to manipulate, but since it was never added to the hierarchy tree it cannot show on the screen.

Fragment Manager

The fragment exists and is managed by the activities fragment manager.  The fragment manager lives in the activity and is available to all fragments.  It is not directly visible within the Views\Components (nor should you ever add it).  The fragment manager manages the fragment lifecycle, addition and deletions, restorations, and the back stack.  The fragment back stack provides a way to stack the fragments within an activity and allow the user to use the back button to pop them off.

Fragments are found via two methods : findViewById() or findViewbyTag().  If you added a fragment without a tag, you are out of luck on finding it again unless you stored a reference to the fragment.  But if you stored a reference to it, then you wouldn’t need to find it again anyway, so that is a moot point.  At least until a configuration change happens and then you are out of luck again.

The id part of findViewById() does not refer to the id of the container for the fragment.  It is the id of the fragment itself.   This is only really useful for finding fragments inflated from the xml:

<fragment android:name="yourFragment"
android:id="@+id/yourfragmentId"
android:layout_width="wrap_content"
android:layout_height="match_parent" />

For all dynamically added fragments, you must use findFragmentByTag().  It is best practice to give every fragment a unique enough tag that you can find it again.


Fragment Transactions


Fragments are updated in the Fragment Manger asynchronously via transactions.  A transaction provides a lot of methods to add, replace, or delete a fragment.  You can manipulate multiple fragments in a single transaction.  It also allows you to customize the entry and exit animations for the fragment.  A standard Fragment transaction will always follow this pattern:


FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
//manipulate transaction
transaction.commit();

A DialogFragment transaction that will show it as a Dialog looks like this:


DialogFragment dialogFragment = new YourDialogFragment();
dialogFragment.show(getFragmentManager(), TAG);


Big Caveat 1


One very important thing to remember is that the Fragment Manager has a drop dead date for adding or removing fragments.  That date expires once the activity calls its onSaveInstanceState().  After that you cannot (reliably) add or remove a fragment.  It will throw a nasty exception. 


There is a grudging workaround : commitAllowingStateLoss()/dismissAllowingStateLoss().  They can work, but may lead to bigger problems.  The most notable being a crash on restore or a zombie fragment.  Between the two, the zombie is the more common occurrence.  This typically occurs when the fragment was killed after the onSaveInstanceState(), but the thing resurrects itself during a restore.


Big Caveat 2


Fragments can be also added via the xml.  Any fragment added this way cannot be moved or deleted.  It exists for the life of the UI it is attached to. These are normally found via its id and not its tag.


The Lifecycle


When it comes to fragments, you must understand the lifecycle.  There are a lot of states and functionality may only be available within certain states of the lifecycle.


The basic lifecycle can found in the android developers guide at http://developer.android.com/reference/android/app/Fragment.html#Lifecycle.  This gives a high level view of the life cycle, but it misses a few states.  There is a lot more going on that you will need to know to make complex fragments.  Here is a more verbose version of the fragment lifecycle:


Fragment Lifecycle - New Page


And here is how it relates to the activity lifecycle:


Activity to fragment states - New Page


Common Pitfalls


Pitfall 1: The Lifecycle


Fragments have a very specific lifecycle that is bound to its Activity.  Unlike Activities, fragments can’t guarantee some fundamental things are not null all the time.  For instance, it is a good thing to know that getView(), getActivtiy(), getFragmentManager(), getString() can and will return null at various inconvenient points.  It is always a good idea to check the state before blinding using any native method unless you are absolutely sure what it is not null. 


The biggest issues I see in Fragments are directly related to not understanding the lifecycle and how it affects the functionality.  Trust me, it is better to think it through up front because it is difficult to retroactively fix lifecycle issues.


Pitfall 2: Commit drop dead date


This is directly related to Pitfall 1, but deserves its own bullet point.  This was discussed in Big Caveat 1 above.


Pitfall 3: DialogFragments


There are two big problems here:



  • Not breaking up the dialogfragment creation correctly.  Most of the creation should be done just like a regular fragment.  Only use the onCreateDialog method to manipulate the dialog window.  Do not perform the UI creation or restoration there. 

  • Issue 2 is how to get the data back to a listener.  The fragments do not support the normal dialog listeners.  You will have to add them or broadcast the results.  If you use listeners, you must handle the restore case correctly.  When the fragment destroys on configuration change, then relationship to the listener is lost.  Either take the fragmentdialog down and restore from interested parties, or broadcast the results.

Pitfall 4:  Is anyone there?


Once a fragment is added to the activity, you may want to find it again.  This can be tricky to do because you either need a tag or id to find it again.  The id only exists on a fragment if it was inflated from the xml.  Your only other option is to tag the fragment. 


Along the same lines, I strongly suggest you come up with unique enough tags to avoid a collision between tag names.  Since fragments can also create other fragments, you need to make sure that you won’t accidently lose a fragment by a reusing tag name accidentally.


Pitfall 5:  #$@ &%! Context


Unfortunately, the fragment does not come with a context.  This is the single biggest pain to deal with when coding fragments.  Context is used everywhere and no great way to get at it that works all the time. 


There are a few lesser-of-evils ways to get it.  They all have Pros and Cons, but a mix of the options will normally get the job done.



  • The most obvious one is getActivity().  Just be aware that this can return in the wrong lifecycle phases.

  • The next option is getView().getContext(), but the getView() can also be null in certain lifecycle-phases and it also only really works if the fragment provides a UI.

  • The third option only works in onCreateView() but it is foolproof : just steal it from the LayoutInflater via inflater.getContext().

  • The fourth option is to use the Application context.  This only works if you override the application class and open it up to pass the context via a static method.  Be careful with this option. It works great, but it can lead to big garbage collection issues if you use it for creating UI elements.

Once you wrangle a Context, never store it in the fragment.  It can seriously mess up your garbage collection.


Pitfall 6: Who’s your daddy?


Inherently all fragments exist in a flat structure.  Basically they are all siblings within the Activity regardless of who created it.  So if the Activity creates Fragment1 and then Fragment1 begetsFragment2.  Fragment2 is not bound in any way to Fragment1.  They are independent siblings.  If Fragment1 is destroyed but does not explicitly take Fragment 2 with it then Fragment 2 will still exist out in the ether (potentially) unknown to anyone. 


You can use child fragments to fix this, but they only came in during Jellybean and there have been reports of problems with them.  I haven’t played with them enough to give a yea or nay about using them yet.


Pitfall 7: Support Fragments vs Native Fragments.


In general, I advise to always use the support library over the native whenever possible.  It updates more often the the native libraries and as a result, bugs get fixed more often.  My only exception to this rule is the Fragments.  For these I don’t have a strong opinion, but I do prefer to use the native Fragments.


Honestly, It doesn’t matter which you use, as long as it is consistent everywhere. Do not ever mix support and native fragments or methods.  It creates all sorts of havoc and weird compilation and runtime errors.

Wednesday, April 1, 2015

MyNotes: Pass the Butter

I recently posted an entry about incorporating Roboguice.  Well, I changed my mind about that and decided to swap it for ButterKnife after all.   After doing some UI work, it became apparent that Roboguice was overkill for MyNotes and wasn't going to work really well in the CommonLibrary.

After some thought, I decided that ButterKnife was a better fit.  Here are the steps I used for swapping out the libraries:

Step 1: Undo the RoboGuice Integration.

I just reversed the steps from the blog entry to return to a clean slate.

Step 2: Bring in ButterKnife

Update the MyNotes gradle.build file with the following:

   release {
     // Library specific proguard files
     ...
     proguardFiles 'proguard-butterknife.pro'
   }
   ....

   dependencies {
      ....  
      compile 'com.jakewharton:butterknife:6.1.0'
    }
Add proguard-butterknife.pro to the project with the following content:
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$$ViewInjector { *; }

-keepclasseswithmembernames class * {
    @butterknife.* <fields>;
}

-keepclasseswithmembernames class * {
    @butterknife.* <methods>;
}
Incorporate it into the MainActivity:
    /**
     * UI element to display the note data.
     */
    @InjectView(R.id.activity_main_notes_recycler_view)
    RecyclerView mCardsRecyclerView;


    /**
     * UI element to allow a user to create a new note.
     */
    @InjectView(R.id.activity_main_add_view)
    View mAddView;


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


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

Commits: