Writing a Custom Android UI Inheritance Lint Rule

Roderik Lagerweij
6 min readMar 14, 2019

--

Do you recognise the tedious feeling of completing a user story and peer-reviewing your code leads to lengthy discussions and refactors? At my last job I was working as developer with seven other feature teams on a mobile banking application. We were doing trunk based development, so no branches. In this kind of setup, alignment and communication is key or you will end up in the situation mentioned before. As substitute for lengthy pull request reviews I went on a ‘shift-left’ search for tooling which could help us in this alignment.

Quickly I started noticing how effective lint is. Thanks to the lint integration in Android Studio developers get immediate feedback from rule violations just after the code is being typed. Consider here the speed of feedback compared to having a pull-request review after an entire user story has been developed.

As an early experiment I created two custom lint rules for two fundamental problems that I found in the project. To both my surprise and delight, I noticed that after having the detectors added to the project the developers stopped violating this rule. This to me was an indication I was on the right path.

The point I am trying to make in this post is that writing custom lint rules applicable to your project is not that difficult and very effective as communication/alignment tool. In this post we will create a simple lint detector that detects when an activity or fragment does not directly inherit from Android’s activity or fragment.

Before we do that let me shortly clarify why I chose this rule. Out of all coding decisions I have taken I always regretted applying inheritance in the user interface layer. A typical situation would be that we would have two similar screens that just have a few differences between the two. In an effort to DRY things up we would extract the common parts of both screens and put those in a base activity/fragment. This might seem convenient at the time but in reality over time it is one of the least flexible and unmaintainable solutions, often leading to spaghetti code where the contract between superclass and it’s implementations is unclear. Rather, I would search for a solution in composing: extracting pieces of code into separate reusable classes, keeping the two similar activities and just use them as glue code for tying the different components together. This definitely requires more thinking on how to split pieces of code into having their own responsibility. But that’s a good thing.

Setup

Let’s start with a quick overview for the lint library that will contain our lint rule that will detect when an activity does not directly inherit from Android’s activity:

1. In Android Studio create a new Android application (an empty activity is just fine)

2. Add a java library module and name it “lintrules”. This will library will contain our custom lint rule.

3. Modify build.gradle file in the lintrules library until it looks something like the following:

Btw. I chose Kotlin for development here.

4. Now we need to define a registry which contains the lint issue we are going to build. We can start with an empty registry as the following:

Since we haven’t written our lint rule yet we just return an empty list for issues. Also we need to tell it the current api constant so that the hosting lint environment knows against which version of the lint api our code is compiled.

Next we need to tell lint where it can find our issue registry. We do this by setting the “Lint-Registry-v2” attribute in build.gradle to our registry which is located in the com.rl.lintrules package:

5. Now to tie the Android app module to the lint module add the following dependency to your app’s build.gradle file in the dependencies section:

Note: in my case it often takes some time before Android Studio catches up and recognises the lint rules, even after synchronising Gradle multiple times. With some patience it always works in the end though.

Writing the lint rule

For writing our lint rule we take the following test driven development approach:

1. Create an empty detector that does nothing

2. Write a test that violates our rule and run our detector on it. Confirm the test fails

3. Implement the minimum code that makes the test green

4. Rinse and repeat: repeat steps 2 and 3 by adding more examples of rule violations and non-violations and update your detector accordingly to keep the tests green

Following step 1, we first start with defining our lint issue and creating an empty detector that does nothing:

Now we can write our first test that violates the inheritance rule. For our tests we will use the convenient LintDetectorTest API:

In this piece of code we define an activity SomeActivity that inherits from a BaseActivity that in turn inherits from the AppCompatActivity. Note that we include in the files the activityStub. Since the tests are not aware of any Android classes we do the following trick to define our own ‘activity stub’ like:

Running this test of course confirms it is red:

Let’s start with our first try at the detector by always giving an error if we are an activity:

In this code we first tell lint that we are only interested in class nodes and not other types of nodes as methods, imports, etc. Then we add an optimisation saying that this lint detector should only be called when this class is of type android.support.v7.app.AppCompatActivity or inherits from this type. Running this we get:

The reason we are now getting three errors is that in our test code SomeActivity, BaseActivity and the AppCompatActivity (from our stub) are all flagged as errors.

Let’s refine our rule and say that if the detector is ran on the AppCompatActivity itself then it should not give an error. We do this by getting the qualified name of the node declaration through the JavaContext:

This gives:

One step closer, but we still have one error too many. This is because BaseActivity gives an error while it shouldn’t since it directly inherits from the AppCompatActivity. Let’s finalise this by specifying that if the class has a parent and this parent is not the AppCompatActivity to only then give an error:

Which makes the test green!

Now we can add other test cases to verify the behaviour of the detector such as the following two:

With the code we have written in the previous steps these tests are already green so there is no need to update the detector. To have a look at what the final version might look like after some cleanup checkout the following repository.

Extending the detector to also detect unnecessary inheritance in fragments is trivial now.

Word of caution: if your project contains both Java and Kotlin code then make sure to write tests for both. While both types of code are roughly parsed the same way there are sometimes small differences that might make your detector work on the one language but not on the other.

Conclusion

As you can see it is relatively easy to setup a lint rule that prevents code from being created that can cost you hours to refactor and untangle later. At the same time, the feedback is given while coding instead of at a pull request review.

Hope you enjoyed this post, happy to help to the best of my ability if there are questions!

--

--

Roderik Lagerweij

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