Introducing Bourbon: Dribbble, Android, MVP and a Common-Code Module

Having used MVP in my Android projects for a short while now, I’ve been wondering about reusing Presenter classes when creating applications for more than just Mobile devices. For this reason, I built Bourbon as an experiment to see the kind of code that I could share between different application modules. I’m happy to say that the result turned out to be pretty positive as I was able to share a huge chunk of the applications logic between the Mobile, Wear and TV modules. Without going too in-depth, I decided to put together this article in-order to share my experiment with others.

The article is split into two sections. In the first, we’ll take a quick look at the Bourbon app and what it does - this allows you to see the visuals of the app first. Then in the second part, we’ll take a look at how the code is shared between the different app instances and how the common code module is structured.

What is Bourbon?

Bourbon is (probably yet another) Dribbble client that supports Android wear, TV and mobile (it’s also optimised for tablets!). If you don’t know dribbble already, it’s a community led design platform that allows designers to share their latest creations in the form of ‘shots’. The purpose of Bourbon is to display the latest 20 shots to the user, allowing them to browse for something inspiring from their chosen Android device.

Bourbon is Open Source!

But why?

I created this project with the idea to demonstrate code re-use across the different application modules - alongside MVP this has proved to be a valuable approach. Whilst I wanted to reduce the codebase for this experiment, I also wanted to ensure that the project was still easily maintainable and the code still understandable to read - which is something that could easily be broken when splitting code into modules.

When I originally set out to just create a dribbble client for Android TV - as MVP goes, a single activity / fragment structure would have looked something like this:

I’m not going to go too much into MVP here, but in a nutshell we have our activity / fragment which implements our MvpView - this is the interface which defines the methods that our activity / fragment is required to implement. This then allows our Presenter and activity / fragment to communicate with one another.

But, I decided I wanted to do something more than create another app for just the TV, so I thought why not create the app for both Mobile and Wear too! This could get very messy though, without sharing any code amongst the app modules we could’ve ended up with something like this 🙀:

This is just a minimal view of the issue also. There would have been other multiple classes too - such as the DataManager, Data Models, API Service and Dagger Injection components / modules etc.

That’s a lot of duplication, especially when the 3 different application modules are going to have exactly the same functionality. Whilst this is the case that the Presenter and MvpView classes are exactly the same, wouldn’t it be much better to do something like this?

So, we still have our individual activity / fragment instances but we’re going to share the same MvpView and Presenter classes, meaning that they only need to be defined once for use with the three application modules. That’s much better, right? 😺

We’ll look more into the technical details of Bourbon and it’s structure further into this article, but to begin with let’s begin by taking a look at the pretty stuff.

Browse Dribbble Shots

The first screen of the app is simply the Browse Screen. This screen displays a stream of shots to the user, along with the title and like count. This works similarly across the different modules, with the retrieval of the shots being completely handled by the shared module - meaning the logic used to do so is only defined once for the three different application modules. In this case, our BrowseMvpView defines the following interface method:

void showShots(List<Shot> shots);

This method is then called from our presenter (which is also shared) when an the shots have been retrieved after making a call to the BrowsePresenter’s getShots() method:

@Override
public void onSuccess(List<Shot> shots) {
...
if (!shots.isEmpty()) {
getMvpView().showShots(shots);
} else {
...
}
}

The showShots() method implementation in each of our application module Browse screen classes can then define how it wishes to handle the display of shots after they have been successfully retrieved:

From Top-Left to Bottom-Right: Mobile, TV, Wear and Tablet

Handling Browse Errors

As you probably already know, things don’t always work out when your app is dealing with remote data. To handle that case, we simply let the user know that something went wrong and allow them to attempt a reload of data if they wish so. Because the Error State is trigged through the BrowsePresenter, this state is handled by that shared presenter class for all of the 3 app modules. In this case, our BrowseMvp view defines the following interface method:

void showError();

This method is then called from our presenter (which is also shared) when an error occurs after making a call to the BrowsePresenter’s getShots() method:

@Override
public void onError(Throwable error) {
...
getMvpView().showError();
...
}

The showError() method implementation in each of our application module Browse screen classes can then define how it wishes to deal with the error that has occurred:

From Top-Left to Bottom-Right: Mobile, TV, Wear and Tablet

Handling Browse Empty States

Sometimes there might just not be any content that we can display to the user. To handle that case, we simply let the user know that there were no shots to display, allowing them to check again if they wish so. Because the Empty State is trigged through the BrowsePresenter, this state again is handled by that shared presenter class for all of the 3 app modules. In this case, our BrowseMvpView defines the following interface method:

void showEmpty();

This method is then called from our presenter (which is also shared) when an empty list of shots is returned after making a call to the BrowsePresenter’s getShots() method:

@Override
public void onSuccess(List<Shot> shots) {
...
if (!shots.isEmpty()) {
...
} else {
getMvpView().showEmpty();
}
}

The showEmpty() method implementation in each of our application module Browse screen classes can then define how it wishes to deal with the empty state that has occurred:

From Top-Left to Bottom-Right: Mobile, TV, Wear and Tablet

View Shot Detail with Comments

When a user selects a Shot from the Browse screen, they’re taken to a Shot Detail screen where they can view the shot and any comments that may exist for it. Again, this works exactly the same across the 3 different application modules so I was only required to define the logic for this screen once.

From Top-Left to Bottom-Right: Mobile, TV, Wear and Tablet

As you can see above on mobile and tablet, the comments for a shot are simply shown in a scrollable list beneath the shot image. However, on TV and Wear we take a slightly different approach - this is simply due to the use of the screen estate on these two platforms. In both cases, we simple use a ViewPager to display comments in a singular fashion. On Wear the user can simply swipe through the comments, whereas on TV the D-Pad can be used to navigate through the comments.

Shot Comments on Wear and TV

In this case, our ShotMvpView defines the following interface method:

void showComments(List<Comment> comments);

This method is then called from our presenter (which is also shared) when a list of comments is returned after making a call to the ShotPresenter’s getComments() method:

@Override
public void onSuccess(List<Comment> comments) {
...
if (comments.isEmpty()) {
getMvpView().showEmptyComments();
} else {
getMvpView().showComments(comments);
}
...
}

The showComments() method implementation in each of our application module Shot screen classes can then define how it wishes to deal with the display of comments once they have been retrieved.

Sharing code with a ‘Common’ module

As you can see from the screenshots above, the app works pretty much the same across the difference device platforms. Imagine if we had to duplicate the code for each of these, there would be a lot of repeated code - luckily I managed to avoid this. Because our application modules all share the same logic, Bourbon uses a CoreCommon module which allows us to share these different classes across the 3 different application modules:

BourbonApplication

This is a standard Android application class that I’ve simply re-used for each of the application modules. This essentially uses Dagger to set up our ApplicationComponent and Timber for logging purposes.

Data Models

Seeing as our application modules are all going to be displaying the same data, it makes sense for them to share the Data Models. There are only 4 (minimal) models used in the application (Shot, User, Comment, Image) but sharing them in this module makes it easier to maintain them if they change at any point.

DataManager

The DataManager class acts as a middle-man for communication with the BourbonService. Again, the application modules all access the same data so sharing the DataManager is just logical.

BourbonService

The BourbonService states the endpoints and manages the retrieval of data from them. So as above with the DataManager, the behaviour is the same across application modules.

Dagger Injection Components and Modules

Seeing as we now know our three application modules use the same DataManager, BourbonService etc - it only makes sense to also share the logic related to Dagger injection. If you look at the injection package, you’ll see that there are several classes declaring components and modules, meaning that the same dependancies can be injected across the applications.

Base Presenter and MvpView

Bourbon uses base classes for Presenters and MvpViews that should be used when created new classes of these kinds. For this purpose, using them through the CoreCommon module ensures that all classes are extending or implementing the same base classes - this again also reduces code duplication.

BrowseMvpView & BrowsePresenter

The Browse screen for each of our application modules behaves in exactly the same way. A list of shots is retrieved and that list if displayed to the user - however, showing / hiding progress indicators, making an API request and correctly displaying with any empty / error states to the user. This means that the Presenter classes will contain the same logic and the MvpView interfaces will define exactly the same interface methods. Fir this reason it makes sense for both the BrowseMvpView & BrowsePresenter to be kept in the CoreCommon module so that these classes only need to be defined once to be shared across our application modules.

ShotMvpView & ShotPresenter

The same applies to the screens used to display Shot Details. The class and interface used to handle the display of content on the screen, so we share the ShotMvpView and ShotPresenter through the CoreCommon module.

Colors, String & Dimension files

Bourbon has specific branding colors, so this isn’t going to change across it’s applications modules. The same also goes for the Strings used throughout the application, there are also some Dimension values that also hold true to this. Because of this, I’ve placed these values in resource files in the CoreCommon module — meaning that they can be shared across the application modules. Now if any of these colors or Strings needs to be changed, I only have to do it once!

TestDataFactory

The TestDataFactory is a class used to construct dummy data models that are used in both the Unit and Instrumentation tests. For this purpose this class exists in the CoreCommon module, which is where the AndroidTestCommon module can access this class from.

Unit Tests

Because the classes requiring unit test are found in the CoreCommon module, the Unit Tests can also be found here. A separate package contains tests for the DataManager and Presenter classes defined in the CoreCommon module.


Project Structure

The Bourbon project structure makes it easy to navigate around it’s codebase. Let’s take a quick look at how it’s split up into modules:

  • CoreCommon - This module contains all of the core app logic and data management classes that are shared across the mobile, tv and wear applications.
  • AndroidTestCommon - This module contains the classes which are shared between the three AndroidTest modules.
  • Mobile - This is the module for the Mobile application.
  • Wear - This is the module for the Wear application.
  • TV - This is the module for the TV application.
  • mobile-AndroidTest - This module contains the instrumentation tests for the Mobile application.
  • wear-AndroidTest - This module contains the instrumentation tests for the Wear application.
  • tv-AndroidTest - This module contains the instrumentation tests for the TV application.

Browse Screen Structure

Let’s take a quick look at the structure of the Browse Screen to make things a little clearer. The steps of the Browse Screen Flow are as follows:

Hopefully from this example you can see just how similar the different application modules are and how beneficial it is that this code has been shared.

If this wasn’t the case then we’d have to individually define:

If you’re looking to achieve a similar result in your application, it’s fairly easy to create a module to share common code - the best place to start for this is the CoreCommon module and the build.gradle file found in its root.

Testing Bourbon

In the GitHub repository you’ll find both Unit and Instrumentation tests, details on how to run these are found in the README.

Unit Tests

As previously mentioned, the Unit tests are located in the CoreCommon module. This is due to all of the classes that require Unit testing being located in that module, so it only feels right for the tests to be located there also. The current Unit Test classes can be found at:

Instrumentation Tests

Of course Bourbon has instrumentation tests! If you hadn’t already noticed, the instrumentation tests for each application module is split out into it’s own individual instrumentation androidTest module. These are located at:

And to follow the theme of code sharing, we also have an AndroidTestCommon module. This is used to share common code which is used amongst the three androidTest modules. This still requires some further utilisation to make the most out of, but the foundations are there.

What’s next for Bourbon?

Bourbon has been great as an experiment, but I’m looking forward to extending on it’s functionality, here’s a few things I plan on adding:

  • Pagination - Currently only the latest 20 shots are retrieved from the API, this should ideally be paginated so the user can enjoy endless viewing.
  • User profiles - It’d be great to have a user profile screen so that you can navigate from a Shot to view more Shots by the selected user.
  • Animation & Screen Transitions - I love animation so therefore it only seems right for me to get around to implementing some!
  • And anything else I think of…

Conclusion

I enjoyed Bourbon as an experiment and would love to hear peoples opinions on this approach. I’m exciting to implement further features into the application and refactor more of the code so that more can be shifted over to the CoreCommon module.

Feel free to drop me a tweet or leave a response below if you have any questions! P.s. Don’t forget to hit the recommend button if you enjoyed this article :)

Check out my other projects at hitherejoe.com