Sharing data and customizing dialogs in Android

A while ago, I was asked to take over an Android project that was outsourced by an indie freelancer. Despite me having no previous experience with Android programming, I was quite shocked when I received the source code. It’s probably the most unorganized and unstructured piece of code I have ever seen. The whole structure more resembled a C project than a Java project.

I’ve heard from some of my colleagues that it’s quite hard to maintain a lightweight class structure in Android because of the Activity classes. Nonetheless, seeing this code I saw so many beginner mistakes that I just can’t let it go unnoticed. I’m quite sure that if I asked the code author what a MVC is, he wouldn’t have a clue. Anyway, seeing so many errors I decided I’d point out some of them in hope that somebody less experienced than me (which might not be a lot of people, since I’m an active programmer for just… 6 years?) finds something useful.

1. Sharing data

Our application is basically a workflow of consecutive steps, each represented by its own Activity. They, however, all operate (edit, read) on the same data. My predecessor solved this problem by creating public variables in the main Activity, and then referencing them from all the other activities. I didn’t particularly like that, so I wrote my own solution. As long as the application lives on Android (even if it’s pushed into the background, paused etc.), a single Application class will exist. That’s the object we need to store our custom data in (as long as we don’t want to use the database, which is really overkill for many smaller apps)! I created my own Application class that looks like this:

MyApplication.java

public class MyApplication extends Application {

    private SharedData sharedData;

    /**
     * Called when the application is started.
     */
    @Override
    public void onCreate() {
        super.onCreate();

        // Initialize a new instance of shared data,
        // only once per application lifetime
        this.sharedData = new SharedData();
    }

    /**
     * Get the shared data.
     * @return The shared data.
     */
    public SharedData getSharedData() {
        return this.sharedData;
    }
}

SharedData is simply an object holding various variables & structures that are needed throughout the application. To ease the things a little bit, I also created a base Activity class from which I extend all my application activities:

MyActivity.java

public class MyActivity extends Activity {

    private SharedPreferences sharedPreferences;

    /**
     * Get the application-wide shared data.
     * @return The shared data object instance.
     */
    protected final SharedData getSharedData() {
        return ((MyApplication)getApplication()).getSharedData();
    }

    /**
     * Get the application-wide shared preferences.
     * @return The shared preferences object.
     */
    protected final SharedPreferences getPreferences() {
        if (this.sharedPreferences == null) {
            try {
                this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
            } catch (Exception e) {
                this.sharedPreferences = null;
            }
        }

        return this.sharedPreferences;
    }
}

What that means is that whenever I need to access the shared data from any activity, I simply call the this.getSharedData() to retrieve or update it. I also added a function that kind of shortens the retrieval of the local preferences.

2. Separation of dialogs

Since our application uses quite many dialogs that are mostly just customized dialogs made from DialogBuilder factory, I still hated seeing all of them hard-coded inside individual activities. Since couple of activities use same dialogs, there was also some code duplication which is never hood. Solution? Isolate dialogs into individual classes:

MyDialog.java

public class MyDialogFragment extends DialogFragment {

    // region EVENTS INTERFACE
    /**
     * An interface, used to contain abstract method definitions which are fired on specific events from the dialog and
     * should be overriden in the calling Activity class.
     */
    public interface MyDialogEvents {
        public abstract void onSubmit(String someDataToPass);
        public abstract void onCancel();
    }
    // endregion

    // region STATIC INITIALIZATION
    /**
     * Initializes a new instance of MyDialogFragment.
     * @param dialogData1 the dialog data 1.
     * @param dialogData2 the dialog data 2.
     * @param target the target events interface.
     * @return the dialog fragment.
     */
    public static MyDialogFragment newInstance(
        ArrayList dialogData1,
        boolean dialogData2,
        final MyDialogEvents target) {
        MyDialogFragment fragment;
        Bundle args;

        fragment = new MyDialogFragment();
        args = new Bundle();
        args.putStringArrayList("dialogData1", dialogData1);
        args.putBoolean("dialogData2", dialogData2);
        fragment.setArguments(args);
        fragment.setTarget(target);
        return fragment;
    }
    // endregion

    // region FIELDS
    private MyDialogEvents target;
    // endregion

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Builder builder;
        Dialog dialog;
        ArrayList dialogData1;
        boolean dialogData2;

        // Retrieve parameters from the bundle
        dialogData1 = this.getArguments().getStringArrayList("dialogData1");
        dialogData2 = this.getArguments().getBoolean("dialogData2");

        builder = new Builder(this.getActivity());
        builder.setTitle(getResources().getString(R.string.title_string));

        // Add custom controls to dialog

        // Add OK & Cancel buttons
        builder.setNegativeButton(getResources().getString(R.string.dialog_negative_default),
            new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int whichButton) {
                    target.onCancel();
                }
            });
        builder.setPositiveButton(getResources().getString(R.string.dialog_positive_default),
            new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int whichButton) {
                String dataToPass;

                // Read the data from the controls, do validation if needed

                // Fire the event
                target.onSubmit(dataToPass);
            }
        });

        dialog = builder.show();
        return dialog;
    }

    /**
     * Sets the target.
     * @param target the target interface.
     */
    public void setTarget(MyDialogEvents target) {
        this.target = target;
    }
}

Since you usually want to do some stuff after dialog is validated/confirmed/cancelled, I exposed an interface with those specific events that the user must implement in the calling code. As I’m writing this code I realized I could move the abstract method definitions into the DialogFragment class itself, and replace the static initialization with the one in the DialogFragment constructor. But oh well, this works perfectly as well. If you want to prevent the dialog from closing when the user clicks the positive/negative button, see this StackOverflow entry (answer #31). To open the given dialog, the simple following code is needed in the calling Activity:

MyDialogFragment dialogFragment;

dialogFragment = MyDialogFragment.newInstance(passedData1, passedData2, new MyDialogEvents() {
    @Override
    public void onSubmit(String someDataToPass) {
        // Do something with data after dialog submission
    }

    @Override
    public void onCancel() {
        // Do nothing on cancel
    }
});

dialogFragment.show(getFragmentManager(), TAG + "_MyDialogFragment");

Considering I spent almost 3 whole weeks to refactor the beast, there were many more modifications & improvements, but I’m too lazy to list them all at once :) . So for now that’s it folks!

Leave a Reply