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.
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.
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.
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.
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
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.
It is also necessary that we visit only the
<color> element and only the attributes where it can be used.
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.
Let’s write our lint detector.
Create a new detector
Define the issue
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
bavkground attributes even if it’s used with elements other than
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
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
Go through all the color usages
visitAttribute method is called for all the instances of
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.
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:ignoreis 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.
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.
- Get started with Android Lint - custom lint rules
- Android Lint Deepdive - advanced custom lint rules