Android Refactor – Part 1

(As a side-note to start with, kind of like a disclaimer – I am not an expert with Android by any means, and I’m sure there are many different ways to refactor this app. This is my first attempt to fix this but I’m doing it for my benefit and to get up and running with my posts!)

I’m going to start off with the model of my Android app. I only have 1 model class, that being the Sound class. It originally operated in a singleton pattern, so I basically had a single Object in my app which I constantly changed. This way won’t work for the RecyclerView, and it’s just damn-right messy code. It stinks. I can also add some more information into the model that will definitely help me later on. Alongside mSoundResourceId, I will add a mImageResourceId field, which will hold the R.drawable id of the image. Generate a getter and setter for the mImageResourceId field, and I will also create a Sound Constructor that will take 2 ints as parameters; soundResourceId and imageResourceId. I have also renamed the class to Meme.java (as you can see by the constructor) to align with the context of the app.

private int mSoundResourceId;
private int mImageResourceId;
public Meme(int soundResourceId, int imageResourceId){
    mSoundResourceId = soundResourceId;
    mImageResourceId = imageResourceId;
}

Although the final layout of the app will be almost identical to what it was before, the way which it is made will be completely different. The hard-coded ScrollView will be gone, along with all of the hard-coded ImageViews and LinearLayouts that go with them. Instead, there will be a single RecyclerView in the XML layout file, and each item (Image/Sound) will be shown in the RecyclerView using a separate XML file. Doing it this way will result in some intimidating code for some new Android developers, but it is mostly boilerplate and actually results in a much more maintainable codebase.

Android app Resources

I’ll start with the ID lists for creating the Sound objects, which will be used to generate the views in the RecyclerView. Rather than hard-coding them in various places, I will create 2 string-arrays in my res/values/strings.xml file. By doing this, I can access these arrays in code easily, while also making it very easy to add in new images/sounds. To add a new one, all I must do is add an item entry to both arrays, and it will be added to the list of sounds in the RecyclerView almost like magic. It provides a clean separation between data and logic, where the resource IDs are accessed in a single location.

<resources>
    <string-array name="meme_audio_source_list">
        <item>@raw/et</item>
        <item>@raw/bradberry</item>
        <item>@raw/copberry</item>
        <item>@raw/brrrradberry</item>
        <item>@raw/hilabradberry</item>
        <item>@raw/ethanbradberry</item>
    </string-array>
    <string-array name="meme_image_source_list">
        <item>@drawable/pointyberry</item>
        <item>@drawable/bradberrypoint</item>
        <item>@drawable/copberry</item>
        <item>@drawable/brrradberry</item>
        <item>@drawable/hila</item>
        <item>@drawable/ethan</item>
    </string-array>
</resources>

We can see here that the only requirement is that the images and sounds are placed in the same correct order in both string arrays, so that the correct meme clip is played when a user selects from the RecyclerView To retrieve these IDs, I will use the getResources().obtainTypedArray(int id) method, which returns a TypedArray that can be looped through to get all of the IDs within. Before we do that, though, I want to rearrange the setup of the application.

Android Activity/Fragment Construction

Currently it’s just a single Activity with all of the code lumped into it; I think it can be better. I might want to, at some point in the future, add multiple collections of memes separated by tabs. I’m going to set up my app so that this would be easier to do in the long run as I think it’s a real possibility to do. After reading The Big Nerd Ranch’s Guide (Thoroughly recommended for beginners/intermediate Android developers!) there was a single-activity approach which I really liked, and it made a lot of sense. By putting the unique content into a Fragment, it’s easy to swap content in and out of the Activity, rather than opening and closing a new Activity each time. As well as that, if I did implement a TabView I would need to convert everything to Fragments anyway. My first task here is to create a new Activity, called SingleFragmentActivity

public abstract class SingleFragmentActivity extends AppCompatActivity {
    protected abstract Fragment createFragment();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fragment);
        FragmentManager fm = getSupportFragmentManager();
        Fragment fragment = fm.findFragmentById(R.id.fragment_container);
        if(fragment == null){
            fragment = createFragment();
            fm.beginTransaction()
                    .add(R.id.fragment_container, fragment)
                    .commit();
        }
    }
}

 

SingleFragmentActivity will extend AppCompatActivity (which in turn extends FragmentActivity) and will now be the superclass of any Activity I need to create. The createFragment() method is abstract and is there to act as a receiver of a Fragment from a subclass. Now we can have many Activities extending SingleFragmentActivity, all creating different types of Fragments, which SingleFragmentActivity accepts and adds it to it’s layout file using a transaction in the onCreate() method. Simples.
Using this as a superclass means I can reduce my ListActivity (the Activity that will hold the Fragment containing the list of memes) to just a few short lines as shown below.

public class MemeListActivity extends SingleFragmentActivity{
    public static final String TAG = "MemeListActivity";
    @Override
    protected Fragment createFragment() {
        return new MemeListFragment();
    }
}

I don’t even need to override the onCreate() method with this Activity, all I need to do is implement the createFragment() method and return a new instance of the desired Fragment. Here’s a high level step-by-step process of my app right now:

  1. MemeListActivity is opened on launch, as specified in the AndroidManifest.xml
  2. When the Activity lifecycle hits the onCreate() stage, because MemeListActivity does not override it, onCreate() gets called in SingleFragmentActivity, the superclass of MemeListActivity
  3. onCreate() here calls createFragment(), which in this case returns a new instance of MemeListFragment (I will go through this now)
  4. This fragment is then inserted into the Activity layout, which has a single root element inside – a FrameLayout that acts as a fragment container
  5. We should then, hopefully see a list of memes that we can listen to as much as we want!
Android RecyclerView StaggeredGridLayoutManager gif
RecyclerView StaggeredGridLayoutManager

The Fragment

My MemeListFragment contains all of the logic associated with the images, sounds, and the RecyclerView/ViewHolder/Adapter.
In total, it’s a little under 100 lines of code including all the import statements which isn’t bad at all. At this first stage of refactoring, I haven’t focused on everything, but it will be a more progressive work-through to the end result. Some of the member variables here can be local, the naming isn’t followed fully, and an array of other improvements can be made in the next few posts.
In the onCreateView() method, I set up the few member variables, such as the RecyclerView, the TypedArrays (which are then used to create the List of Memes) and the RecyclerView Adapter. There’s nothing out of the ordinary here, the for-loop uses one of the TyedArray‘s length as the counter, as both arrays will be the same length. I use a StaggeredGridLayoutManager for the RecyclerView‘s layout, which gives the list a nice look to it, similar to Pinterest/Tumblr/Masonry layouts on websites.

private MemeAdapter mMemeAdapter;
    private RecyclerView mMemeRecyclerView;
    private List<Meme> mMemeObjList = new ArrayList<>();
    @Override
    public View onCreateView (LayoutInflater inflater,ViewGroup container, Bundle savedInstanceState){
        View v = inflater.inflate(R.layout.fragment_meme_list, container, false);
        mMemeRecyclerView = (RecyclerView) v.findViewById(R.id.meme_recycler_view);
        mMemeRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL));
        mMemeRecyclerView.setHasFixedSize(true);
        TypedArray spicyMemes = getResources().obtainTypedArray(R.array.meme_audio_source_list);
        TypedArray spicyMemesImages = getResources().obtainTypedArray(R.array.meme_image_source_list);
        for (int i = 0; i < spicyMemes.length(); i++) {
            int memeSoundResoureceId = spicyMemes.getResourceId(i, -1);
            int memeImageResourceId = spicyMemesImages.getResourceId(i, -1);
            Meme meme = new Meme(memeSoundResoureceId, memeImageResourceId);
            mMemeObjList.add(meme);
        }
        if(mMemeAdapter == null){
            mMemeAdapter = new MemeAdapter(mMemeObjList);
            mMemeRecyclerView.setAdapter(mMemeAdapter);
        }
        return v;
    }

The ViewHolder

The ViewHolder is part and parcel of implementing a RecyclerView. For every item in the RecyclerView, there is a ViewHolder there which stores information about the View. The MemeHolder inner class is a bog-standard implementation of RecyclerView.ViewHolder, with only an ImageView and a Meme object as member variables. (I may add a TextView in the future) In the MemeHolder constructor, I set an onClickListener (implemented by the MemeHolder class) and get a reference to the ImageView from the itemView parameter. The onClick method for the ViewHolder starts up the MediaPlayer with the sound ID taken from the Meme object. The final method in MemeHolder is bindMeme() which sets the member variable Meme to the parameter included in the method, as well as setting the ImageDrawable source on the ImageView. A pretty basic implementation so far, but that’s what’s needed right now.

private class MemeHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
        private ImageView memeImageView;
        private Meme meme;
        public MemeHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(this);
            memeImageView = (ImageView) itemView.findViewById(R.id.meme_image_view);
        }
        @Override
        public void onClick(View view) {
            MediaPlayer mp = MediaPlayer.create(getActivity(), meme.getSoundResourceId());
            mp.setOnCompletionListener(new android.media.MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(android.media.MediaPlayer mp) {
                    mp.release();
                }
            });
            mp.start();
        }
        public void bindMeme(Meme meme){
            this.meme = meme;
            memeImageView.setImageDrawable(getResources().getDrawable(meme.getImageResourceId()));
        }
    }

The Adapter

The Adapter is arguably the most important part of a RecyclerView. It creates the Views/Holders to be used in the RecyclerView, inflating the correct layout from the res/layout folder. In my Adapter I have a member variable of a List of Memes which is initialised in the constructor with the List made in the onCreateView() method of the Fragment. This list is used to bind the objects to the views so that the correct image/sound is shown. Other than that, the Adapter is probably the most basic implementation that can be done. My Android app doesn’t have a very complex setup, but I have already made it much easier to manage in the future.

private class MemeAdapter extends RecyclerView.Adapter<MemeHolder>{
        private List<Meme> mMemeList;
        public MemeAdapter(List<Meme> memes){
            mMemeList = memes;
        }
        @Override
        public MemeHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
            View v = layoutInflater.inflate(R.layout.list_item_meme, parent, false);
            return new MemeHolder(v);
        }
        @Override
        public void onBindViewHolder(MemeHolder holder, int position) {
                Meme meme = mMemeList.get(position);
                holder.bindMeme(meme);
        }
        @Override
        public int getItemCount() {
            return mMemeList.size();
        }
    }

 

Recap

I started off with a very large layout file, full of ImageViews, nested LinearLayouts and a ScrollView. It was a very messy setup and hard to maintain. A single Sound object was kept in MainActivity, which was used to play the correct sound. As the Activity implemented OnClickListener, I had a large onClick method with a switch statement full of all the possible cases. Based on these cases, the Sound object resource member variable was updated, and played using MediaPlayer.

I now have an Activity with a single FrameLayout view in the layout file, which is replaced by an instance of a Fragment containing ONLY a RecyclerView and a RelativeLayout root in it’s layout. In this fragment, all of the resources are loaded into an Object List from the strings.xml file and given to the RecyclerView to create and show the Views/ViewHolders. The onClick functionality is handled by each ViewHolder, and uses the Meme getter to play the correct sound resource.

I’ve made my life easier to add new sounds in the future – all that needs to be done is:

  1. add the .ogg file to the res/raw directory
  2. add the Image to the res/drawable directory
  3. add the IDs to the string-arrays in strings.xml (IDs being the name of the files)

And that’s it! A much simpler process than before.

If you want to look over the whole Android project, I have the entire app on my Github profile!

The next post will contain some tricks to use Android Studio to help keep my project nice and clean, and some work on the design of the app in its current form.