The first hurdles of a new project can be essential to its future. Assuming you already have a great idea for an app in mind, you probably want to open up Android Studio and get coding! But before we engage the practical side of things, we’re going to have to look into some essential theory first (sorry not sorry).
Clean Code
You’ve probably heard someone speak of the ‘Clean Code’ concept before. It’s a term coined by Robert C. Martin (also known as “Uncle Bob”) who also wrote a book named after it. Should you ever have the time to read it, it’s definitely a recommendation and an eye-opener in quite some regards.
But what is ‘Clean Code’? Even after reading the book, it’s still hard to give it a definition considering it’s heavily dependent on context. Uncle Bob even mentions it in the book himself:
There are probably as many definitions as there are programmers
So how are you supposed to know you’re ‘doing it right’? Well, this StackOverflow answer seems to align pretty close to how I would summarise it:
Easy to understand.
Easy to modify.
Easy to test.
Works correctly (Kent Beck’s suggestion — very right).
To get a better grasp on these requirements, let’s break them down below.
Easy to understand
In his ‘Clean Code’ book, Uncle Bob states the following:
“Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …[Therefore,] making it easy to read makes it easier to write.”
Now I don’t know about you, but I can definitely align with the statement that my time spent reading, evaluating & understanding code takes up several times of the amount of time I spend actually writing code. Once you understand this concept, it reveals the necessity of writing understandable code. The more convoluted, nested & bloated your code gets, the more time you and your colleagues will have to spend getting to understand it.
A classic case here is also the coder who forces ‘complex & eloquent’ code which only helps to confuse the reader, rather than make the code more understandable. It can be compared to the writers who seem as if they paraphrase their original writings using a thesaurus, only to create the perception of eloquence & intelligence to the reader while the written content itself loses power & context. A great code example of this archetype can be found on the Simplethread blog:
The essence is pretty clear, but there is no added value to using bitshift & bitwise operators here.
Imagine the author wouldn’t have added the comment. How long would it take for you to figure out what this function would do? Alright, now let’s take a look at an alternative way of writing the same function:
Standard classes are a-okay!
How long does it take you now? Do you still consider the previously written comment to be of necessity here? The code should simply document itself, and comments should never be used as an excuse to write convoluted/complex code. In the cases where you deem adding comments a necessity, always document the ‘why’ of your code and never the ‘how’. The latter will almost always be objected to change, while explaining ‘why’ a certain piece of code needs to work a certain way will be a lot less volatile in nature.
Easy to modify
While easy readable code will make also make for more easily modifiable code, there are some structural implications as well. Let’s look at the following example:
I think that already at a first glance we can observe some things which are pretty bad for easy modifiability:
A lot of code repetition
Monolith copy-paste methods, instead of opting for 1 modular getGreeting() method
Now let’s take a look at how we could improve this class below:
As you can see, the code has been modularised quite a bit and we need to make a lot less changes when adding a language in the future, or when we want to change something about the validation part. Pre-/appending parts to our name argument will immediately apply it to all languages as well.
Another change made here is replacing the returning of null with the throwing of an IllegalArgumentException. This is more a matter of taste in my opinion, rather than a ‘good change’. Just make sure that whichever approach you take, it aligns with the rest of the codebase for consistency.
Easy to test
Testing is quite a controversial subject in the Android community. Despite everyone preaching its necessity, it still happens far too rarely. And one of the main contributors to this, is the the common lack of structure, modularity & architecture in codebases. Effective unit testing, calls for an effective codebase.
Should your business logic be implemented inside of your activity or fragment, you also need to have an instance of this activity or fragment during unit testing. A solution to this could be using instrumented testing or preferably a UT framework like Robolectric, but an even better solution is decoupling your business logic & models from Android itself.
All of this and more will be discussed in the following chapters!
Works correctly
I think this one is pretty self-explanatory. If your code doesn’t behave according to the requirements, it’s bad. An effective solution to track this behaviour is consistent unit testing and maybe even introducing a process like Test-Driven Development.
Now that we’ve pretty much covered all 4 requirements, let’s take a look at how we can apply these in a practical way.
Clean Architecture
In this section we will discuss the inherent properties of a “clean architecture” and how it’s built up. We will use the Model-View-Presenter (MVP) architecture as a reference considering it’s one of the most balanced architectures in Android in terms of “code quality” to “knowledge required”.
If you’ve ever looked into clean architectures before, you might have encountered a similar image. If you have no experience with clean architectures yet, it might seem a bit confusing and maybe even overwhelming at first, so we’re going to break this diagram down (from inner core to outward circle).
A very important thing to remember here is that the diagram represents dependencies. So the most outer circles can have a dependency on the most inner ones, but the Entities for example can never have a dependency on an element in any of the other circles. The Android SDK/NDK should only start appearing from the third (red) circle and preferably even the most outward one. The fewer elements have a dependency on Android itself, the easier both development as well as testing will go.
Architectural Layers
There are 3 main architecture layers tied to a classic Android app (A classic application would for example retrieve data from a source upon an event, and displays this data on the screen):
Domain layer
Data layer
Presentation layer
Domain
The domain layer is the most important layer within your application and is completely decoupled from the Android operating system and any framework. This basically means that it has no dependencies on or imports outside of your own package. There is one exception to this rule, and this is the coupling to the core libraries. More info about this will follow later once we start implementing our architecture. But for now, just remember that the domain layer mainly consists of your own code.
Inside of the domain layer, we will mostly find our Entities & Models as well as our Use Cases.
Data
The data layer is the layer that relates to data sources and data storage. This is where we’ll place the classes which we use to communicate with databases, REST-services, shared preferences, … Such a class is called a ‘Repository’ (not to be confused with git-repositories).
In general, the interface/API we use to communicate with the repository is implemented in an interface, and belongs to the domain layer. The Repository Implementation itself is pure data layer.
Presentation
As you might have guessed, the presentation layer is related to everything dealing with the UI/View aspect of your application. Most of the time, these will be classes such as your activities, fragments, adapters,… but also your Presenters & View interfaces. The activities & fragments within your application will implement a View-interface which contains the API for showing the outcome/result of your Use Cases.
Now that we have a rough idea of what the layers are about, let’s take a look at a visual representation of what a typical flow in our application would look like:
Entities / Models — Domain Layer
Entities is basically a synonym for ‘models’, and a model is nothing more than a POJO business object (or for example a data class in Kotlin). It’s compromised of nothing more than getters/setters, and its only use within the application is to represent a data structure. Let’s take a look at an example entity below:
In the example above, we’ve created a very basic representation of a person. Here we only keep track of a person’s first & last name as well as his age and address. And a peculiar thing here, is that the address of the person is also represented by a different entity rightfully named ‘Address’. Entities are very important in database management.
If you have experience with DB management systems, you’ve likely encountered an Entity Relationship Diagram or Entity Relationship Model before. This diagram is a visual representation of how the entities within a database or application are related to each other. Should we make an ERD for our example above, our Person entity would have a dependency on our Address entity, which in turn could for example have a dependency on a Country entity.
The entities/models are the most commonly shared elements within your application and any class/component is allowed to import them.
Use Cases (aka Interactors) — Domain Layer
Use Cases are nothing more than implementations of features, and are sometimes also known as interactors. These classes basically contain your application’s business logic, and they should always have 1 dedicated task as to conform to the Single Responsibility Principle defined by ‘Uncle Bob’.
An example could be a class called GetPersonFromDatabaseUseCase. And as the name might already suggest, the dedicated task of this class is to retrieve a Person entity from the database and return this to the calling Presenter. Use Cases usually also implement some form of validation, so they can let the Presenter know whether the retrieval was a success and if the returned object conforms to the expectations. Some Use Case examples will be demonstrated in the following chapter.
Presenters — Presentation Layer
When we look at the 2 most basic (and known) architectures in Android, we end up with MVP (Model-View-Presenter) and MVC (Model-View-Controller). I won’t go to deep into their differences & characteristics at this point but if you really want to look into it, Florina has already done an excellent job in her respective MVC and MVP articles. I’m using MVP as a reference for this article, but I won’t do MVC considering I find its paradigm to be a bad match with Android in general.
Presenters are components within your application which are responsible for addressing Use Cases and View interfaces. When a certain event is triggered (f.e.: OnClickListener), the event should call a Presenter method, and the Presenter will in turn instantiate and execute the correct Use Case. Just as with the Repository, the Presenter’s API is defined within an interface which is part of the domain layer. Once the Repository has retrieved the value, it will address a callback and the value will be sent back through the Use Case to the Presenter, and the Presenter will in turn (if necessary) morph the state of the value to something which is displayable before finally returning it to the View interface to be consumed by your activity/fragment. So the Presenter is actually responsible for notifying your application of any events taking place, and morphing/adapting the returning data to a structure that is consumable by your View.
To clarify the morphing/adapting part, let’s imagine that your UI contains a RecyclerView. And as you probably know, to display data using a RecyclerView, you first need to create an adapter. This adapter will hold both the data that needs to be displayed, as well as the structure it needs to be displayed in. So when your data returns from the Repository and arrives in the Presenter, the presenter first needs to take all this data and create an adapter. And only once that is done, it should pass the adapter to the View. The sole responsibility of a View is to display data. It shouldn’t be concerned with what the data looks like and should just show it on the UI. So leave all data manipulations to the Presenter.
Repositories — Data Layer
A repository usually has a simple interface, but can have a complex implementation which is invisible to the calling component (usually a Use Case). An example would be when we would like to retrieve a ‘Person’ object, we could call a method named ‘getPerson(String lastName)’. And despite this call being very simple in nature, the back-end could involve some complex operations such as checking whether the person is available in cache, and if not retrieving it from a server or cloud storage first before returning the object. Depending on the actual complexity of the app, the Repository implementation will differ. It the job is rather simple (retrieving data from 1 source), then there is no real need to fragment the Repository itself. But if the implementation involves several sources, it usually a good idea to have the Repository be a delegate between the different sources.
View Interfaces & Implementations / DB / Network Source / Adapters / … — Data & Presentation Layer
The outer circle of our diagram contains our components which are basically fully entwined with the Android SDK/framework or maybe some third-party libraries/SDK’s. This is also the layer where your activities and fragments (View implementations) are located, along with their View interfaces. If you use any custom View components, they belong here as well.
Now that we’ve covered most of the components here, let’s take a look at how this architecture would translate in terms of project structure.
Project Structure
Despite being a reasonably important item in my opinion, project/package structure is an often overlooked element. It’s definitely not a blocker in any way or form, but the way you structure your classes and components can definitely give your team a much better oversight and idea of how your application works at a first glance.
The latter statement also implies that this part will mostly revolve around the way I interpret and recommend structuring your application. If you believe that a different way is better, do it that way. Whichever way works best for you (and your team!) is more important than following someone’s opinion.
For the past 2 years now, I’ve been working with a system I’ve dubbed “Architecture-Layer-Based Package-by-Feature” (ALBPF in short). Giving it a name also allows me to place a small annotation in my README.md files to quickly have an idea how this project will be structured and implemented. (F.e.: Architecture: ALBPF-MVP ; or — Architecture: ALBPF-MVVM ; …)
Now let’s break ALBPF down:
Architecture-Layer-Based
This part basically implies that the first layer of abstraction inside my project structure will based upon the Architecture Layer as discussed previously above. So before we even start creating functional packages, we will incorporate an architectural division first:
This division will enforce us to think about our architecture more strictly and also allows for certain modularity which you wouldn’t get with other structures. Due to the fact that all of our models & business logic will be located within the domain package, we can actually extract the domain package out of the application and use it as a separate module or library even. So if your application is getting too big or if a different application needs to reuse your business logic, it’s a very simple procedure to share the code between the two.
And as mentioned previously, considering we know what type of component belongs in which layer, we know that we can find all Presenters inside of the presentation package, all Repositories inside of the data package and all Use Cases within the domain package.
Package-By-Feature
When thinking of project-packaging, there are 2 general schools of thought:
Package-by-Layer
An in my opinion very poorly named implementation, which involves packaging your components by their component type (rather than layer). I still believe Package-by-Component or something in the likes is more appropriate. An implementation of this would look as follows:
As can be seen, it’s not a bad approach but I find the abstraction to be too large to be used as a first or even second level abstraction. This is generally the most common way projects get structured considering it’s also the easiest way to do it. It can definitely work, but why settle for ‘ok’ when we can do better? Let’s take a look at the next option.
Package-by-Feature:
This paradigm is often promoted, yet more often misunderstood. It’s difficult to implement in the beginning because you need to get used to a certain way of thinking first. But after the ‘click’ in your brain takes place, magic happens. Let’s imagine a hypothetical situation where a client asks you make him an app for office inventory management, with the following requirements:
Managing ‘Employee’ objects
Managing ‘Inventory’ objects
Log-In functionality
Local caching of objects
Displaying Employee info
Displaying Inventory info
Alright, so that seems like a pretty basic requirement for such an application and it can also be covered by the whole MVP spectrum. If we break these requirements down into common denominators, we end up with the following:
Employee
Inventory
Login
These 3 common denominators will form the baseline for our Package-by-Feature structure. If we would create our packaging now, it would look like this:
So what is the big advantage to this you might think? The first and foremost one, is Separation of Concerns. When we create this application, we have to make sure that every component has a single & tightly defined responsibility. And as you might’ve presumed, this responsibility will coincide with one of the 3 previously defined ‘features’.
And I hear you coming: “But how can it be a single feature when it could have to handle different CRUD operations and have to create, assign, delete and update Employees or Inventory objects”? This is a very valid question, and is one of the big struggles people have with Packaging-by-Feature.
You have to realize that such operations can be considered features on a project level, but they are nothing more than Use Cases on a technical level. Whether you create, assign, delete or update an employee, chances are very big that these operations will share a lot of commonalities. Let’s create a few dummy classes to show what our project structure would look like with actual components for the Employee feature:
These classes & interfaces are likely the ones I’ll need to implement the Employee feature as required. And as you can see within the Employee-package, I sometimes create an extra “Package-by-Layer”-package such as adapter or model, to denote the type of classes that can be found there.
Another observation is how our well our current project structure aligns with the previously laid out theory. This structure really reflects the theory and enforces you to apply it right.
So if we would extrapolate the current Employee-feature to our previously defined flow, it would look like this:
And another big advantage to this approach is that it’s extremely easy to port features over to different projects. Considering every feature is well-defined & isolated and the only objects features can share among each other are models from the Domain-layer, we only need to copy over the feature’s layer-packages to the new project and you’re set!
Scaling
So what happens when a feature starts to grow in size? Let’s say our Employee feature now has to handle even more user scenario’s and has been divided into a separate listview and sheetview. Well in that case you keep the common elements in the main package and you make a division where necessary:
In this situation the UseCases & Repositories will probably remain the same, but the biggest impact will be on the presentation side. So we just divide the components into two separate list & sheet packages with their own dedicated components. In a different situation, the same could happen with the Repository.
Now we only have 1 central component, but if we would actually have to implement caching & server synching, we would probably use the EmployeeRepository component to address separate components like EmployeeDataCache and EmployeeCloudStorage in a ‘cache’ and ‘cloudstorage’ package respectively.
Conclusion
It might feel like a lot of information to process at first, but try to read a couple of times through it until you really think you have a good grasp on the concept. This information should already give you a good idea of what MVP, Clean Architectures and Package-by-Feature are all about. It’s a minor investment for the return it will bring you.
Comentários