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.
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:
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:
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:
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.
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.
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:
- In each of our Activity / Fragment classes, we begin by calling the BrowsePresenter’s getShots() method – this Presenter class is found in the CoreCommon module.
- In this method, the Presenter uses the implemented BrowseMvpView showMessageLayout() method to tell our Activity / Fragment to remove any Error / Empty state message from the layout.
- Next, the Presenter uses the implemented BrowseMvpView showProgress() method to tell our Activity / Fragment to show the loading indicator to the user.
- Then the presenter calls the DataManager’s getShots() method to retrieve the latest dribbble shots from the API.
- If this request errors, then the implemented hideProgress() BrowseMvpView method is called. This is followed by the showError() method.
- If the result is successful then the implemented hideProgress() BrowseMvpView method is called. Then depending on whether or not there are results returned, the showShots() or showEmpty() methods are called.
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:
- The MvpView interface
- The BrowsePresenter class
- The Shot, User and Image data models
- The DataManager class
- The BourbonService class
- Various Injection logic
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:
- BrowsePresenterTest – Unit Tests for the BrowsePresenter class
- ShotPresenterTest – Unit Tests for the ShotPresenter class
- DataManagerTest – Unit Tests for methods in the DataManager class
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:
- mobile-androidTest – Instrumentation tests for the Mobile module.
- wear-androidTest – Instrumentation tests for the Wear module.
- tv-androidTest – Instrumentation tests for the TV module.
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