Android , Development , Process

The Lowdown on Android Lint

August 13, 2019

I’m a big fan of using auto­mat­ed tools to improve the qual­i­ty of code our teams write. Often this includes unit test­ing and code for­mat check­ers. But, for the pur­pos­es of this blog, I’m going to cov­er one kind of tool in par­tic­u­lar: lin­ters. Though some tools con­flate lint­ing with code style check­ing, in real­i­ty a lin­ter is tool that ana­lyzes code for poten­tial bugs or unde­sir­able behavior.

(Quick note: While this post focus­es specif­i­cal­ly on lint­ing Android source code, keep an eye out for future posts on lint­ing for oth­er frame­works and languages.)

Elim­i­nat­ing Bugs with Android Lint #

The folks on the Android Devel­op­er Tools team have giv­en us a great tool for lint­ing Android code, one that’s inte­grat­ed direct­ly into the build sys­tem. In fact, you are already using this tool and prob­a­bly don’t even know it! Some of the checks it includes run by default when you are edit­ing Java or Kotlin source code or XML lay­out resources in a project. One exam­ple is when you don’t use an @string resource for user-vis­i­ble text in a lay­out resource file:

There is also a sub­set of the avail­able lint rules that check for issues which cause run­time crash­es if not addressed, such as call­ing APIs that are unavail­able on old­er plat­forms with­out an SDK_INT check or try­ing to start an Activity that isn’t reg­is­tered in the AndroidManifest.xml file. These vital” checks run auto­mat­i­cal­ly as part of a release build in the lintVitalRelease gra­dle task.

It’s great get­ting all of these tools for free. Then again, humans are eas­i­ly dis­tract­ed and don’t always pay atten­tion to appro­pri­ate warn­ings when edit­ing source files. Wouldn’t it be nice if these checks ran auto­mat­i­cal­ly every time we com­mit changes to our source repos?

Run­ning Android Lint in CI #

At Michi­gan­Labs, our Android projects run lint as part of the Git­lab CI pipeline that is trig­gered on every new com­mit to our Git repo. We also retain the HTML and XML arti­facts from lint so the devel­op­er doesn’t have to re-run the lint tool in order to see the for­mat­ted results, not to men­tion 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 suc­ceed” in the merge request checks sec­tion of the repos­i­to­ry set­tings to pre­vent merge requests with fail­ing tests or lint checks from being merged.

Cus­tomiz­ing Lint Checks #

By default, the lin­ter will only fail if there are error sever­i­ty lev­el issues found in your project. There are numer­ous oth­er checks that default to a lev­el of warning (or below) which, if detect­ed, would ide­al­ly fail a build. We can do this using a cus­tomized lint con­fig­u­ra­tion file. The lint.xml file con­tains a list of issues as well as the lev­el of sever­i­ty vio­la­tions should be report­ed at. To make the hard­cod­ed text warn­ing into an error, sim­ply insert <issue id="HardcodedText" severity="error" /> into the file.

Below is the lint.xml file we cur­rent­ly use for our projects that should offer a base­line for your projects. The Android team main­tains a list of avail­able checks, also vis­i­ble in the Android Stu­dio pref­er­ences dia­log under Edi­tor > Inspec­tions > 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 direc­to­ry of the project, it is applied auto­mat­i­cal­ly. Our teams often go a step fur­ther and add options that lead to more thor­ough checks, mak­ing the out­put more effec­tive in CI logs. These options are con­fig­ured 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
  }
  // ...
}

Get­ting Exist­ing Projects Up To Speed #

You may find that adding this set­up to your exist­ing projects leads to hun­dreds or even thou­sands of errors. By baselin­ing the project, you can cap­ture a snap­shot of the exist­ing errors and, more impor­tant­ly, weed out new ones. This makes adding Android lint checks to your project vir­tu­al­ly cost-free.

When edit­ing files in Android Stu­dio, the base­line con­fig­u­ra­tion is ignored, allow­ing you to iden­ti­fy and address old errors. My advice is to fix the lint errors in files that you are already touch­ing for oth­er rea­sons, allow­ing you to address issues in the base­line file down the road.

Extend­ing Lint Checks to Oth­er APIs #

Libraries, such as Tim­ber and Work­Man­ag­er ship their own cus­tom lint rules to avoid incor­rect or inef­fi­cient usage of their APIs. The best part is that Android lint auto­mat­i­cal­ly detects these new rules and applies them to your build. Note that you may still need to tweak the sever­i­ty set­tings on the new rules so that, if vio­lat­ed, your build will fail.

You can also pro­vide your own lint rules as the devel­op­er of an Android library. I won’t cov­er that in this post, but Big Nerd Ranch offers insight on how to write your own cus­tom lint checks.

Tak­ing the Pain Out of the Dev Cycle #

Some of the issues that Android lint checks for can take a while to ana­lyze (such as unused resources requir­ing checkDependencies to be enabled); how­ev­er, run­ning the checks in the back­ground after a com­mit in CI is a good way to take the pain out of the local devel­op­ment cycle.

Look­ing ahead, the Android tools team has been work­ing on Project Mar­ble, a way to improve the per­for­mance of the lin­ter and oth­er build tools. We can’t wait.

Ready to get started?

Call us at 616-594-0269 or send us a note below.
Visit our office @ 452 Ada Drive SE Suite 300 Ada, Michigan 49301
Send us an e-mail @ info@michiganlabs.com