For the past five years, our Android project has maintained a similar package structure from when it was first created in 2012. There have been a few changes here and there, but the general structure has remained the same.
When most applications are created, the class count is small. However, as the app grows and features get added, it can be easy for packages to become bloated, potentially to the point where it makes a workspace feel unorganized and difficult to navigate.
This was starting to be true of our Android app.
As well as keeping our code clean, we also want to keep our package structure clean. This rethought is about keeping the workspace that we interact with on a daily basis both tidy and organized.
So we set about to rethink the entire package structure of the Buffer Android app. I’m excited to share what our process looked like and all that we learned, and it’d be great to hear your thoughts and questions, too.
The prevailing structure of the Buffer Android app (what things looked like before we reorganized)
Before we get into the details of the restructure, let’s take a look at the structure of the app before any improvements began.
The package structure was essentially built around the classes being grouped by their corresponding type: activities in an activities directory, fragments in a fragments directory and so on. It looked something like this:
It’s still quite common to come across projects being structured in this way. It does seem logical doesn’t it? Our class names always end with their corresponding type, so why not group them like that!
Well, all may appear well-and-good while the application class count per directory remains small, but the tricky part is that the count will grow as your app grows. This causes the packages to become bloated and unorganized, and in time, it makes it difficult to navigate and feel any sense of structure to a project.
Our new structure helps to achieve the complete opposite effect of the older, traditional setup. Within the new structure, packages are lean and organized, and navigation within the project stays crisp and smooth.
For example, with the improved navigation, now when we’re working on a feature, we often remain with the directory that we need to work in without the need to switch back and forth between directories. While there are still iterations and improvements to be made, we’re hoping that this new approach to organizing our project will save us from pains in the long run. ?
Here’s a look at the new version of our overall base package structure for the Buffer Android app:
Now we essentially have four layers of organization for our classes.
While the names may speak a little for themselves, let’s take a look at the purpose of each package along with the kind of classes and child directories that they may contain.
The four new layers of organization for our classes
The data package contains any classes (and child packages) that are directly related to any kind of data or data management used within the app — be it networking classes and interfaces, preferences management, database classes, data models, network request and response model, or anything else directly tied to app data.
The model package is used to hold all of the model classes for any data objects that are used within the app. These could be models returned from network requests, or models that are dynamically created when we’re saving draft updates to the local database.
And because we deal with models from multiple networks (currently our networks include the Buffer API for our data and the Helpscout API for FAQ data), we have the model package separated into two child packages. This allows us to keep the two model families separated from one another as each child package holds its corresponding models. In the future if we add more, it’ll be simple to add another child package to the model directory.
The local package contains all of the classes that deal with data being persisted locally to the application. These are classes that handle both the storage and retrieval of local data. So in the case of Buffer for Android this is the PreferencesHelper (used to handle the storage and retrieval of preferences values) and DatabaseHelper (used to handle the saving of data to the local database).
The remote package contains all classes that are responsible for holding any classes that are responsible for or involved in saving or retrieving data from an external source via a network request. This can include classes such as retrofit interfaces and factories, OKHTTP interceptors and error classes, or any other classes involved with remote data.
Within the remote package, there is a child response package which holds any response model classes that are by the retrofit service interfaces. Initially these classes were defined as inner classes within the service, but as the requests grew, it made sense to move them into a separate package to be defined within their own class. This removes the responsibility of the interfaces defining the response class and just giving it the single responsibility of providing the interface methods.
The manager package is used to hold a collection of DataManager classes, each of which holds methods returning Observable instances which can be used to interact with the corresponding methods in the remote / local packages to save and retrieve data. We currently have UserDataManager, ComposerDataManager, and HelpDataManager classes, which help to split up the operations from different features within the app. As the app evolves, we will add corresponding DataManager classes to this package.
We keep all of our services inside a separate package, again to separate them from other packages within our project. We currently only have 4 services within our app which have no relation to one another, so there is no child packaging within the service package itself.
The receiver package is used to hold any BroadcastReceiver instances that we have defined for use within our app. Just like the services package, there is no child packaging with the receiver package as we only have several receiver classes being used within our app.
The UI package is responsible for holding any classes that are related to the UI components of the application. Within this package we also have child packages that are organized per-feature. This makes it super tidy and extremely easy to navigate when working with a specific feature. This looks a little like so:
The UI package is fairly simple as it’s unlikely that there will be any classes that do not sit inside of a child package. Here’s a look at what the child packages are used for.
The base package is used to hold any base classes that can be extended by other classes within the UI package. For example, we have BaseActivity and BaseFragment classes which are used to hold common logic for the activities and fragment used by our app, meaning we don’t have to repeat the same code over and over for each new activity or fragment that we create. This package also holds base classes for any Presenter classes and MvpView interfaces that may exist within the child feature packages.
The common package is used to hold any classes that may be used across the different features within the UI package. For example, we have general ErrorView and RefreshingView classes that are shared across several different screens, so placing this inside a common package feels natural.
If necessary, then classes within the common package are also grouped into child packages for further organization, like so:
Each feature child package contains all of the classes that are specific to that feature. We’re only going to look at one feature (the composer), but each feature follows the same approach. The package will always hold any Activities, Presenters, Fragments, or MvpViews that are specific to that feature. For example, the composer package currently holds a ComposerActivity, ComposerPresenter, and ComposerMvpView.
The package also contains two child packages, which contain classes specific to the feature itself. This is because the Composer users a profile selection fragment (displayed as a bar above the composer), as well as a couple of widgets, both of which only contain components that are not used elsewhere in the app. This structure looks like so:
The profiles package above is its own feature package essentially. But it’s specific to the composer, so this is where it currently lives. The profiles package contains its own Activity, Fragment, and Presenter as well as its own child packages for the adapters and other classes that are used and specific to the profiles feature itself.
The injection package holds all of our dependency injection classes. This helps to keep any DI configuration and responsibilities separated from the rest of the application. The package itself holds several child packages to further simplify the organization of our DI files.
The component package holds any Component interfaces that have been defined for use within our DI setup. We currently have an ActivityComponent for activity level dependencies, FragmentComponent for fragment level dependencies, ApplicationComponent for application level dependencies, ViewComponent for view level dependencies, and a Configuration Component for persistence across orientation changes. For this reason, they live in this package to avoid bloating the parent injection package.
This package holds any @Module annotated classes that are used to provide dependencies for our dependency injection setup. We currently have an ActivityModule, FragmentModule, and ApplicationModule, so having this child package helps us to keep the modules contained from the rest of the DI files.
The scope package holds any @Scope annotated classes used alongside the other DI classes. Because we have several defined scopes, we are using the package for the same reason as the other child packages within this injection package.
The util package is used to contain any kind of Helper class or Utility class that we may use for things such as Dialog creation, Snackbar creation, Display metrics, Connectivity checks, Custom Tabs, or any other form of task that falls within a utility class. Currently, there is no feeling of similar responsibility within this package, so no child packages exist while the package class count remains so small.
And that’s the change we’ve made for now! No doubt we’ll iterate and improve on what we currently have as we learn along the way, but we’re looking forward to and excited to be working with a more organized project structure.
There have been some great influences that helped motivate us for this change such as Package by Features, not Layers and Google Samples – Android MVP Clean Architecture. We gained a ton of insight from them and would encourage you to check them out if you’re interested in learning more. ?
We’d love to hear your thoughts on our approach and even see how you’re structuring your projects! Feel free to leave a comment here or tweet us @bufferdevs ?