Saturday, July 6, 2013

DialogFragment displaying options from a cursor held by the Activity

Continuing to chase down performance problems with Android's StrictMode in My Expenses: On several occasions, I needed to present the user a dialog for selecting an item from a cursor. Previously I was loading this cursor in the fragment without passing through a CursorLoader. Instead of loading the cursor again, if it is already held in the activity, I was looking for a way to access the activity's cursor, and came up with SelectFromCursorDialogFragment. It defines an interface (SelectFromCursorDialogListener) that the activity displaying the dialog implements in order to 1) provide the cursor (getCursor) and 2) to process the result (onItemSelected).

An example use case is the "Move transaction" command, that allows to move a transaction from one account to another, and needs to present a dialog for selecting the target account. Following code is used for displaying the dialog

      args = new Bundle();
      args.putInt("id", R.id.MOVE_TRANSACTION_COMMAND);
      args.putString("dialogTitle",getString(R.string.dialog_title_select_account));
      args.putString("column", KEY_LABEL);
      args.putLong("contextTransactionId",info.id);
      args.putInt("cursorId", ACCOUNTS_OTHER_CURSOR);
      SelectFromCursorDialogFragment.newInstance(args)
        .show(getSupportFragmentManager(), "SELECT_ACCOUNT");

SelectFromCursorDialogFragment receives a bundle with the following required keys
id
Allows to identify the context from which the dialog is opened in the onItemSelected callback.
dialogTitle
The title for the dialog.
column
the column being used for the options to be selected.
cursorId
the id under which the cursor will be made available in getCursor.
 
The bundle is handed back in the callback, so any additional information can be added that is needed for processing, in this example we store as "contextTransactionId", the transaction that the user wants to move.
In this example, we implement the interface in the following way

  @Override
  public void onItemSelected(Bundle args) {
    switch(args.getInt("id")) {
    case R.id.MOVE_TRANSACTION_COMMAND:
      Transaction.move(
          args.getLong("contextTransactionId"),
          args.getLong("result"));
      break;
    }
  }
  @Override
  public Cursor getCursor(int cursorId) {
    switch(cursorId) {
    case ACCOUNTS_OTHER_CURSOR:
      return new AllButOneCursorWrapper(mAccountsCursor,currentPosition);
    }
    return null;
  }

The "result" key in the bundle is where the selected item's id is stored.
The cursor returned for the id ACCOUNTS_OTHER_CURSOR is the solution to another challenge. The activity has a cursor for all accounts, but the target obviously needs to be a different account from the current one, so we need to exclude one row from the cursor, which is handled by the AllButOneCursorWrapper class.

EDIT: It turned out, that above solution only worked as long, as the user does not change orientation while displaying the dialog. To make the dialog react correctly to orientation changes proofed to be quite complicated. The problem is, that when the activity is recreated and the fragment instantiated, the cursor is not yet available, and you need to hook into the cursor loader's callback in order to call up the fragment and set the cursor there. SelectFromCursorDialogFragment now implements this requirement through the following changes:
  1. Instead of using the builder's setSingleChoiceItems method, an SimpleCursorAdapter is created and stored as class member variable.
  2. SelectFromCursorDialogFragment defines a setCursor method which uses the adapter's swapCursor method.
  3. the getCursor method of the SelectFromCursorDialogListener interface has a second argument tag which allows the activity to store the tag of the fragment, if it does not have the cursor the fragment needs.
  4. in the activity's onLoadFinished method, we check if there is fragment tag stored as a callback, retrieve the fragment, and use its  setCursor method.
Some overhead for a tiny detail, but worthwhile, if you pay attention, as I try to do, to have your app handle orientation changes correctly.

No comments:

Post a Comment