Android Lint Deep dive - Advanced custom Lint rules
This is the second article in the Android Lint series. In the previous article, we talked about basics of Android Lint tool, how to write a custom lint rule, register the issue and set it up. In this article, we shall explore more about Lint and write a lint rule which is a bit advanced.
Scenario
At work, I was approached by a problem where I had to write a lint rule to detect usage of colors which do not refer to a set of colors provided by the design team. To elaborate more on this, we have a set of colors / color scheme defined by the design team. The app uses these colors, but it also uses some colors which are not provided by the design team, so the lint rule should identify such colors and raise issue for each usage of such colors.
Let’s look at a sample.
Now, when the Lint runs, it should give error for definition and usage of text_color
and definition and usage of primary_color
should be fine as it refers to an approved color.
Approach
I would encourage you to think of a solution for some time before reading further.
Right now, we will just focus on resources and ignore the usage of colors in java/kotlin. We shall extend ResourceXmlDetector
as we are focusing on resources only. The approach, to this problem that I have come up with, goes somewhat like following:
- Find all the declaration of
<color>
element and check which of them refer to approved colors, unapproved colors and hardcoded values. Flag the color, if necessary. - Find all the attributes where colors are supposed to be used, eg. textColor, background, etc.
- Filter the above list of color usage and remove approved colors and raise issue for the remanining usages.
Caveats
The lint tool checks files one after another and we can’t be sure of the order of the files. So we may encounter usage of a color before its definition and incorrectly flag it as an error. Let’s look at the lifecycle
of a Lint issue.
Lifecycle
The “lifecycle” of a lint detector is not as complicated as an Activity and it’s also quite helpful.
beforeCheckProject(Context context)
- This method gets called once per detector when you run lint. Analysis of the project will begin after this method so we should set up necessary things.beforeCheckFile(Context context)
- This method gets called once for each file. This method indicates that analysis of that file will begin after this method and we should perform necessary setup.afterCheckFile(Context context)
- This method gets called after the analysis of a file is finished. We should perform necessary clean-ups and report the issues.afterCheckProject(Context content)
- This method gets called after the analysis of the project is complete. Here, we may perform necessary clean-ups and report project-wide issues.
Other useful methods in the lifecycle:
visitElement(XmlContext context, Element element)
- XmlScanner invokes this method when it visits a particular element. - We will use this method to find declarations of<color>
elements.visitAttribute(XmlContext context, Attr attribute)
- XmlScanner invokes this method when it visits a particular attribute. - This method is helpful as we will obtain usage of various colors through this method.
Preparations
It is also necessary that we visit only the <color>
element and only the attributes where it can be used. getApplicableElements()
and getApplicableAttributes()
are the two methods where we will define our scope of elements and attributes. The detector shall gather all the definition in getApplicableElements()
and all the usage in getAppilcableAttributes()
, curate lists of approved colors and suspicious usage. In afterCheckProject()
method, the detector shall filter the colors and report issues.
Code
Let’s write our lint detector.
Create a new detector
Define the issue
Provide Scope
We need to tell the lint tool what type of elements and attributes the detector expects.
With this configuration, this detector will be invoked for all the <color>
elements in the project. The detecor will also be invoked for colors
, textColor
, and bavkground
attributes even if it’s used with elements other than <color>
.
Setup
predefinedColors
contains all the colors that are defined by the design team. We will add all such colors to the set in beforeCheckProject
method. This method is called before the project analysis starts for this detector.
allowedColors
will be filled with custom colors which refer to one of the colors from predefinedColors
. colorUsages
is a list of Pair (com.android.utils.Pair) which contains Attribute
and its Location
(which is required to repor the issue).
Check all the colors
vistiElement
method is called for all the defined <color>
elements in the project. We need to access the value of that color. We can use element.getTextContent()
to get the value, but it does not work when the Android Studio runs lint. So we access the child node and get its node value. Once we obtain the value of that color, we need to check if it exists in predefined colors or not. If it does, it means that this color is allowed to be used, so we add it to allowedColors
set.
Go through all the color usages
visitAttribute
method is called for all the instances of color
, textColor
and background
attributes. We check if the value (color) of the attribute is in predefinedColors
set. If it’s not present, we add a Pair of attribute and its location to a list for analysis at a later point. We report issue if the value is a hardcoded color.
Analysis and clean-up
afterCheckProject
method gets called once the tool goes through all the applicable files. Here, we go through colorUsages
which contains Pair of attribute and its location about which we are not certain if it’s an issue or not. So we check if the value (color) of the attribute is available in allowedColors
or not. If not, the detector raises the issue.
Hook and run
As mentioned in the previous article - Get started with Android Lint - Custom Lint Rules, it’s really easy to setup lint.
Edge cases
During the implementation and testing I found three cases that merits attention.
- If a color is defined in its own file (eg. for selector, etc), it does not get correctly attributed and its usage is shown as an issue.
- If a color refers to a color which refers to predefined colors, lint will raise error for its definition and usage.
- If a color does not refer to predefined colors and
tools:ignore
is used which makes lint ignore the issue in the report, should lint report its usages as issues or not? Right now, it does.
For the second case, I think it’s a good practice to raise an issue for a chain of references. If you want to make lives of other devs a bit difficult, you don’t fix the 3rd issue and let them add tools:ignore
for each usage. It is imperative to fix the first case.
This article has become a bit longer than I expected, so I leave it to the reader to fix the edge cases.
Here’s the complete code.
Here’s the solution for 1st and 3rd edge case - ColorDetector.
Summary
Lint is a very effective tool for static analysis and can prevent a lot of potential issues and help maintain the code quality. In the next article, I will talk about debugging and generating custom report with lint.
Series
- Get started with Android Lint - custom lint rules
- Android Lint Deepdive - advanced custom lint rules
Redux architecture and Android
Learn the Redux architecture and implement the clean architecture it in your Android codebase. This series of posts explores the depth of Redux and guides you through implementing the architecture in Kotlin for your android app.