I’m a big fan of using automated tools to improve the quality of code our teams write. Often this includes unit testing and code format checkers. But, for the purposes of this blog, I’m going to cover one kind of tool in particular: linters. Though some tools conflate linting with code style checking, in reality a linter is tool that analyzes code for potential bugs or undesirable behavior.
(Quick note: While this post focuses specifically on linting Android source code, keep an eye out for future posts on linting for other frameworks and languages.)
Eliminating Bugs with Android Lint #
The folks on the Android Developer Tools team have given us a great tool for linting Android code, one that’s integrated directly into the build system. In fact, you are already using this tool and probably don’t even know it! Some of the checks it includes run by default when you are editing Java or Kotlin source code or XML layout resources in a project. One example is when you don’t use an @string
resource for user-visible text in a layout resource file:
There is also a subset of the available lint rules that check for issues which cause runtime crashes if not addressed, such as calling APIs that are unavailable on older platforms without an SDK_INT
check or trying to start an Activity
that isn’t registered in the AndroidManifest.xml
file. These “vital” checks run automatically as part of a release build in the lintVitalRelease
gradle task.
It’s great getting all of these tools for free. Then again, humans are easily distracted and don’t always pay attention to appropriate warnings when editing source files. Wouldn’t it be nice if these checks ran automatically every time we commit changes to our source repos?
Running Android Lint in CI #
At MichiganLabs, our Android projects run lint as part of the Gitlab CI pipeline that is triggered on every new commit to our Git repo. We also retain the HTML and XML artifacts from lint so the developer doesn’t have to re-run the lint tool in order to see the formatted results, not to mention the lengthy error report:
Lint:
script:
- ./gradlew lintDebug
artifacts:
name: lint-$CI_COMMIT_SHA
paths:
- ./**/build/reports/lint-results-debug.html
- ./**/build/reports/lint-results-debug.xml
expire_in: 1 month
We also enable “Pipelines must succeed” in the merge request checks section of the repository settings to prevent merge requests with failing tests or lint checks from being merged.
Customizing Lint Checks #
By default, the linter will only fail if there are error
severity level issues found in your project. There are numerous other checks that default to a level of warning
(or below) which, if detected, would ideally fail a build. We can do this using a customized lint configuration file. The lint.xml
file contains a list of issues as well as the level of severity violations should be reported at. To make the hardcoded text warning into an error, simply insert <issue id="HardcodedText" severity="error" />
into the file.
Below is the lint.xml
file we currently use for our projects that should offer a baseline for your projects. The Android team maintains a list of available checks, also visible in the Android Studio preferences dialog under “Editor > Inspections > Android > Lint”.
<lint>
<issue id="ObsoleteSdkInt" severity="error"/>
<issue id="PrivateResource" severity="error"/>
<issue id="UnusedIds" severity="ignore"/> <!-- does not catch uses of view IDs by Kotlin android extensions -->
<issue id="UnusedResources" severity="ignore" />
<issue id="GradleDynamicVersion" severity="error" />
<issue id="CheckResult" severity="error" />
<issue id="SimpleDateFormat" severity="error" />
<issue id="RtlEnabled" severity="error" />
<issue id="Registered" severity="error" />
<issue id="InnerclassSeparator" severity="error" />
<issue id="CommitPrefEdits" severity="error" />
<issue id="HardcodedText" severity="error" />
<issue id="UnusedAttribute" severity="error" />
<issue id="IconLocation" severity="error" />
<issue id="MergeRootFrame" severity="error"/>
<issue id="UseCompoundDrawables" severity="error"/>
<issue id="Autofill" severity="error" />
<issue id="LabelFor" severity="error" />
<!-- Change back to error when https://issuetracker.google.com/issues/110243369 is fixed -->
<issue id="SyntheticAccessor" severity="ignore" />
<issue id="MissingTranslation" severity="ignore" />
<issue id="ExtraTranslation" severity="ignore" />
<!-- No one cares. -->
<issue id="Overdraw" severity="ignore" />
<issue id="GoogleAppIndexingWarning" severity="ignore"/>
<issue id="Typos" severity="ignore" />
<!-- This is nice to have, but it makes your builds not repeatable in the future -->
<issue id="GradleDependency" severity="informational" />
</lint>
If the lint.xml
file appears in the root directory of the project, it is applied automatically. Our teams often go a step further and add options that lead to more thorough checks, making the output more effective in CI logs. These options are configured in your module’s build.gradle
inside the android
section:
android {
lintOptions {
// Write a text report to the console (Useful for CI logs)
textReport true
textOutput 'stdout'
// HTML/XML reports always explain but this gets too verbose in console logs
explainIssues false
// Include the resources from all of its upstream modules in its analysis.
// Required to get accurate unused resource
checkDependencies true
// Also check test case code for lint issues
checkTestSources true
// We run a full lint analysis as build part in CI, so we can skip redundant checks.
checkReleaseBuilds false
}
// ...
}
Getting Existing Projects Up To Speed #
You may find that adding this setup to your existing projects leads to hundreds or even thousands of errors. By baselining the project, you can capture a snapshot of the existing errors and, more importantly, weed out new ones. This makes adding Android lint checks to your project virtually cost-free.
When editing files in Android Studio, the baseline configuration is ignored, allowing you to identify and address old errors. My advice is to fix the lint errors in files that you are already touching for other reasons, allowing you to address issues in the baseline file down the road.
Extending Lint Checks to Other APIs #
Libraries, such as Timber and WorkManager ship their own custom lint rules to avoid incorrect or inefficient usage of their APIs. The best part is that Android lint automatically detects these new rules and applies them to your build. Note that you may still need to tweak the severity settings on the new rules so that, if violated, your build will fail.
You can also provide your own lint rules as the developer of an Android library. I won’t cover that in this post, but Big Nerd Ranch offers insight on how to write your own custom lint checks.
Taking the Pain Out of the Dev Cycle #
Some of the issues that Android lint checks for can take a while to analyze (such as unused resources requiring checkDependencies
to be enabled); however, running the checks in the background after a commit in CI is a good way to take the pain out of the local development cycle.
Looking ahead, the Android tools team has been working on Project Marble, a way to improve the performance of the linter and other build tools. We can’t wait.