Enforcing Clean Architecture Using Android Custom Lint Rules

Roderik Lagerweij
ProAndroidDev
Published in
5 min readApr 26, 2020

--

Does the picture below look familiar to you? I bet as developer we have all felt like this at one point. Having a clear architecture is an essential step in avoiding these scenarios. Today we will look at how to write custom lint rules that enforce architecture principles.

Why are custom lint rules such an effective tool in enforcing coding guidelines? Because they eliminate waste!

http://themetapicture.com/the-life-of-a-software-engineer/

One common architecture that nowadays we see very often in Android applications is Uncle Bob’s Clean Architecture. The following image you might have seen:

If you’re not familiar yet with Clean Architecture I recommend to read up on it before continuing. In short, in Clean Architecture we (usually) define the following three layers:

  • Presentation layer: responsible for presenting to the screen and handling user input
  • Domain layer: responsible for business logic
  • Data layer: responsible for data retrieval and persistence

A key of Clean Architecture is how the dependencies between the layers are organized. In particular, as explained here, we should not let the domain layer depend on either the data layer or the presentation layer. This allows us to easily swap out these outer layers in case this would be needed, e.g., when we switch database implementation.

One way to achieve this is to organize your Gradle modules according to the layers and dependencies; when the domain Gradle module does not have a dependency on your data Gradle module we are sure the dependency rule is not violated. However, what do you do with large, legacy projects where there is no such clear separation and already tons of dependency violations? Restructuring and refactoring the project is too much work to do in one go, but at the same time you want the codebase to improve over time rather than get worse.

A practical strategy for incrementally improving your project is:

  1. Create packages for the 3 layers and move existing classes into the package they belong
  2. Write custom lint rules that detect violations of the dependency rule based on class imports, e.g., a class in the domain package importing a class from the data package is a violation
  3. Create a lint baseline report of the project in the current state
  4. Fail the build when new violations of the lint rules are found

Step 1, 3 and 4 are pretty self-explanatory. We will focus on step 2 and create lint rules for the following two rules:

  1. A file located in the domain package cannot import anything from the presentation package
  2. A file located in the domain package cannot import anything from the data package

Implementation

If you are unfamiliar with writing custom lint rules I recommend to check out a resource on the topic that explains a few more basics, e.g., this or this post.

The two aforementioned rules look quite similar and for that reason we define a single generic interface of which we will provide multiple implementations:

Our rule interface that determines whether an import is valid or invalid

The goal is that after finishing all the steps in this post we should have a lint detector that for every import statement in any file calls the isAllowedImport function. For example, if we have a file LoginUseCase in the com.myapplication.domain package which imports the UserDaoImpl class from com.myapplication.data package then the lint detector should call the isAllowedImport function with the following arguments:

  • “com.myapplication.domain” for visitingPackage
  • “LoginUseCase” for visitingClassName
  • import com.myapplication.data.UserDaoImpl” for importStatement

Since this is a violation of our domain-data dependency rule our implementation should return false.

So let’s provide implementations for our import rules. The first implementation is to detect domain classes importing data classes. For that we use very basic string matching:

Rule implementation that says data imports in the domain layer are not allowed

Very similarly, the implementation for detecting import from domain to presentation package looks like:

Rule implementation that says presentation imports in the domain layer are not allowed

Pretty straightforward, right?

Now the ‘only’ thing left to do is to write the lint detector that actually makes sure our rule implementations are called in the proper way and violations are reported.

We create a new Detector implementation. Here, we provide the list of rules we want to be checked for every import statement. Secondly, we tell the detector we want to handle any import statements by returning the UImportStatement class in the getApplicableUastTypes function:

Setup of our import detector

Now the final step is to provide an implementation for the visitImportStatement function, which should let our import rules verify if there are any import violations and report if so:

Implementation of our visitImportStatement function

Once we visit an import we iterate over all our import rules, collect the information we need and verify with our rules if the import is allowed or not. If an import is not allowed we collect the message which we should show the developer for reference why this is a violation. That is all! This wasn’t too bad, was it? This is what the resulting lint rule looks like in action:

A few details have been omitted in this post for brevity. If you want to check the implementation in it’s entirety check the following GitHub repository:

Conclusion

I hope this post shows how we can relatively quickly write rules that help us enforce architecture. Any comments, questions or feedback? Please let me know in the comments!

Closing notes

  • What other violations can we catch by creating other import rules? Think, for example, a feature independence rule that won’t let feature x import from feature y or vice versa
  • An additional benefit of (custom) lint rules is they allow us to measure progress over time and have transparent whether the state is improving or declining
  • Writing custom lint rules TDD style is very effective!

--

--

Android developer at IceMobile. Looking for more efficient ways of delivering software